mirror of
https://github.com/XFox111/SimpleOTP.git
synced 2026-04-22 08:00:45 +03:00
Major 2.0 (#20)
* New 2.0 version + DependencyInjection library * Updated docs and repo settings (devcontainers, vscode, github, etc.) * Added tests * Fixed bugs * Minor test project refactoring * Updated projects - Added symbol packages - Updated package versions to pre-release * Updated SECURITY.md * Added GitHub Actions workflows
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("SimpleOTP.Tests")]
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace SimpleOTP.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for generating and validating One-Time Passwords.
|
||||
/// </summary>
|
||||
public interface IOtpService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an OTP URI for specified user and secret.
|
||||
/// </summary>
|
||||
/// <param name="username">The username of the user.</param>
|
||||
/// <param name="secret">The secret to use.</param>
|
||||
/// <param name="counter">(only for HOTP) The counter to use.</param>
|
||||
/// <returns>The generated URI.</returns>
|
||||
public Uri CreateUri(string username, OtpSecret secret, long counter = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an OTP code for specified user and secret.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret to use.</param>
|
||||
/// <param name="counter">(only for HOTP) The counter to use.</param>
|
||||
public OtpCode GenerateCode(OtpSecret secret, long counter = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Validates an OTP code for specified user and secret.
|
||||
/// </summary>
|
||||
/// <param name="code">The code to validate.</param>
|
||||
/// <param name="secret">The secret to use.</param>
|
||||
/// <param name="resyncValue">The resync value. Shows how much the code is ahead or behind the current counter value.</param>
|
||||
/// <param name="counter">(only for HOTP) The counter to use.</param>
|
||||
/// <returns><c>true</c> if the code is valid; otherwise, <c>false</c>.</returns>
|
||||
public bool ValidateCode(OtpCode code, OtpSecret secret, out int resyncValue, long counter = 0);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace SimpleOTP.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Provides options for the One-Time Password service.
|
||||
/// </summary>
|
||||
public class OtpOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the issuer.
|
||||
/// </summary>
|
||||
public required string Issuer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The issuer domain.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>IMPORTANT:</b> Using this property will imply adherence to the Apple specification.
|
||||
/// </remarks>
|
||||
public string? IssuerDomain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The algorithm to use.
|
||||
/// </summary>
|
||||
public OtpAlgorithm Algorithm { get; set; } = OtpAlgorithm.SHA1;
|
||||
|
||||
/// <summary>
|
||||
/// The number of digits in the OTP code.
|
||||
/// </summary>
|
||||
public int Digits { get; set; } = 6;
|
||||
|
||||
/// <summary>
|
||||
/// The number of seconds between each OTP code.
|
||||
/// </summary>
|
||||
public int Period { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// The type of One-Time Password to generate.
|
||||
/// </summary>
|
||||
public OtpType Type { get; set; } = OtpType.Totp;
|
||||
|
||||
/// <summary>
|
||||
/// The format of OTP URIs.
|
||||
/// </summary>
|
||||
public OtpUriFormat UriFormat { get; set; } = OtpUriFormat.Google | OtpUriFormat.Minimal;
|
||||
|
||||
/// <summary>
|
||||
/// The tolerance span for the OTP codes validation.
|
||||
/// </summary>
|
||||
public ToleranceSpan ToleranceSpan { get; set; } = ToleranceSpan.Default;
|
||||
|
||||
/// <summary>
|
||||
/// Custom properties to place in OTP URIs.
|
||||
/// </summary>
|
||||
public NameValueCollection CustomProperties { get; } = [];
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System.Collections.Specialized;
|
||||
using SimpleOTP.Fluent;
|
||||
|
||||
namespace SimpleOTP.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for generating and validating One-Time Passwords.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration for the One-Time Password service.</param>
|
||||
internal class OtpService(OtpOptions configuration) : IOtpService
|
||||
{
|
||||
private readonly string _issuerName = configuration.Issuer;
|
||||
private readonly string? _issuerDomain = configuration.IssuerDomain;
|
||||
private readonly OtpAlgorithm _algorithm = configuration.Algorithm;
|
||||
private readonly OtpType _type = configuration.Type;
|
||||
private readonly OtpUriFormat _format = configuration.UriFormat |
|
||||
(string.IsNullOrWhiteSpace(configuration.IssuerDomain) ? 0 : OtpUriFormat.Apple);
|
||||
private readonly int _digits = configuration.Digits;
|
||||
private readonly int _period = configuration.Period;
|
||||
private readonly NameValueCollection _customProperties = configuration.CustomProperties;
|
||||
private readonly ToleranceSpan _tolerance = configuration.ToleranceSpan;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an OTP URI for specified user and secret.
|
||||
/// </summary>
|
||||
/// <param name="username">The username of the user.</param>
|
||||
/// <param name="secret">The secret to use.</param>
|
||||
/// <param name="counter">(only for HOTP) The counter to use.</param>
|
||||
/// <returns>The generated URI.</returns>
|
||||
public Uri CreateUri(string username, OtpSecret secret, long counter = 0)
|
||||
{
|
||||
OtpConfig config = new(username)
|
||||
{
|
||||
Algorithm = _algorithm,
|
||||
Type = _type,
|
||||
Issuer = _issuerName,
|
||||
Digits = _digits,
|
||||
Period = _period,
|
||||
|
||||
Secret = secret,
|
||||
Counter = counter
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_issuerDomain))
|
||||
config.WithAppleIssuer(_issuerName, _issuerDomain);
|
||||
|
||||
config.CustomProperties.Add(_customProperties);
|
||||
|
||||
return config.ToUri(_format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an OTP code for specified user and secret.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret to use.</param>
|
||||
/// <param name="counter">(only for HOTP) The counter to use.</param>
|
||||
/// <returns>The generated code.</returns>
|
||||
/// <exception cref="NotSupportedException">The service was not configured properly. Check the "Authenticator:Type" configuration.</exception>
|
||||
public OtpCode GenerateCode(OtpSecret secret, long counter = 0)
|
||||
{
|
||||
using OtpSecret secretClone = OtpSecret.CreateCopy(secret);
|
||||
|
||||
Otp generator = _type switch
|
||||
{
|
||||
OtpType.Hotp => new Hotp(secret, counter, _algorithm, _digits),
|
||||
OtpType.Totp => new Totp(secret, _period, _algorithm, _digits),
|
||||
_ => throw new NotSupportedException("The service was not configured properly. Check the \"Authenticator:Type\" configuration.")
|
||||
};
|
||||
|
||||
return generator.Generate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates an OTP code for specified user and secret.
|
||||
/// </summary>
|
||||
/// <param name="code">The code to validate.</param>
|
||||
/// <param name="secret">The secret to use.</param>
|
||||
/// <param name="resyncValue">The resync value. Shows how much the code is ahead or behind the current counter value.</param>
|
||||
/// <param name="counter">(only for HOTP) The counter to use.</param>
|
||||
/// <returns><c>true</c> if the code is valid; otherwise, <c>false</c>.</returns>
|
||||
/// <exception cref="NotSupportedException">The service was not configured properly. Check the "Authenticator:Type" configuration.</exception>
|
||||
public bool ValidateCode(OtpCode code, OtpSecret secret, out int resyncValue, long counter = 0)
|
||||
{
|
||||
using OtpSecret secretClone = OtpSecret.CreateCopy(secret);
|
||||
|
||||
Otp generator = _type switch
|
||||
{
|
||||
OtpType.Hotp => new Hotp(secret, counter, _algorithm, _digits),
|
||||
OtpType.Totp => new Totp(secret, _period, _algorithm, _digits),
|
||||
_ => throw new NotSupportedException("The service was not configured properly. Check the \"Authenticator:Type\" configuration.")
|
||||
};
|
||||
|
||||
return generator.Validate(code, _tolerance, out resyncValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
namespace SimpleOTP.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the One-Time Password service.
|
||||
/// </summary>
|
||||
public class OtpServiceConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the issuer.
|
||||
/// </summary>
|
||||
public string Issuer { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The issuer domain.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>IMPORTANT:</b> Using this property will imply adherence to the Apple specification.
|
||||
/// </remarks>
|
||||
public string? IssuerDomain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The algorithm to use.
|
||||
/// </summary>
|
||||
public OtpAlgorithm Algorithm { get; set; } = OtpAlgorithm.SHA1;
|
||||
|
||||
/// <summary>
|
||||
/// The number of digits in the OTP code.
|
||||
/// </summary>
|
||||
public int Digits { get; set; } = 6;
|
||||
|
||||
/// <summary>
|
||||
/// The number of seconds between each OTP code.
|
||||
/// </summary>
|
||||
public int Period { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// The type of One-Time Password to generate.
|
||||
/// </summary>
|
||||
public OtpType Type { get; set; } = OtpType.Totp;
|
||||
|
||||
/// <summary>
|
||||
/// The format of OTP URIs.
|
||||
/// </summary>
|
||||
public OtpUriFormat UriFormat { get; set; } = OtpUriFormat.Google;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use minimal URI formatting (only required, or altered properties are included), or full URI formatting.
|
||||
/// </summary>
|
||||
public bool MinimalUri { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// The tolerance span for the OTP codes validation.
|
||||
/// </summary>
|
||||
public ToleranceSpanConfig ToleranceSpan { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Custom properties to place in OTP URIs.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> CustomProperties { get; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the tolerance span.
|
||||
/// </summary>
|
||||
public class ToleranceSpanConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of periods/counter values behind the current value.
|
||||
/// </summary>
|
||||
public int Behind { get; set; } = ToleranceSpan.Default.Behind;
|
||||
|
||||
/// <summary>
|
||||
/// The number of periods/counter values ahead of the current value.
|
||||
/// </summary>
|
||||
public int Ahead { get; set; } = ToleranceSpan.Default.Ahead;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace SimpleOTP.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the One-Time Password service.
|
||||
/// </summary>
|
||||
public static class OtpServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the One-Time Password service to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="issuerName">The issuer/application/service name.</param>
|
||||
/// <returns>A reference to this instance after the operation has completed.</returns>
|
||||
public static IServiceCollection AddAuthenticator(this IServiceCollection services, string issuerName) =>
|
||||
AddAuthenticator(services, issuerName);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the One-Time Password service to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="issuerName">The issuer/application/service name.</param>
|
||||
/// <param name="configure">The configuration for the One-Time Password service.</param>
|
||||
/// <returns>A reference to this instance after the operation has completed.</returns>
|
||||
public static IServiceCollection AddAuthenticator(this IServiceCollection services, string issuerName, Action<OtpOptions>? configure = null)
|
||||
{
|
||||
OtpOptions options = new()
|
||||
{
|
||||
Issuer = issuerName
|
||||
};
|
||||
|
||||
configure?.Invoke(options);
|
||||
services.AddTransient<IOtpService, OtpService>(_ => new OtpService(options));
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the One-Time Password service to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configuration">The configuration for the One-Time Password service.</param>
|
||||
/// <returns>A reference to this instance after the operation has completed.</returns>
|
||||
public static IServiceCollection AddAuthenticator(this IServiceCollection services, IConfiguration configuration) =>
|
||||
AddAuthenticator(services, configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the One-Time Password service to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <param name="configuration">The configuration for the One-Time Password service.</param>
|
||||
/// <param name="configure">The configuration for the One-Time Password service.</param>
|
||||
/// <returns>A reference to this instance after the operation has completed.</returns>
|
||||
public static IServiceCollection AddAuthenticator(this IServiceCollection services, IConfiguration configuration, Action<OtpOptions>? configure = null)
|
||||
{
|
||||
OtpOptions? options = GetOptionsFromConfiguration(configuration);
|
||||
|
||||
if (options is null)
|
||||
return services;
|
||||
|
||||
configure?.Invoke(options);
|
||||
services.AddTransient<IOtpService, OtpService>(_ => new OtpService(options));
|
||||
return services;
|
||||
}
|
||||
|
||||
private static OtpOptions? GetOptionsFromConfiguration(IConfiguration configuration)
|
||||
{
|
||||
OtpServiceConfig config = new();
|
||||
IConfigurationSection configSection = configuration.GetSection("Authenticator");
|
||||
|
||||
if (!configSection.Exists() || string.IsNullOrWhiteSpace(configuration["Authenticator:Issuer"]))
|
||||
return null;
|
||||
|
||||
configSection.Bind(config);
|
||||
|
||||
OtpOptions options = new()
|
||||
{
|
||||
Issuer = config.Issuer,
|
||||
Algorithm = config.Algorithm,
|
||||
Type = config.Type,
|
||||
Digits = config.Digits,
|
||||
Period = config.Period,
|
||||
IssuerDomain = config.IssuerDomain,
|
||||
ToleranceSpan = (config.ToleranceSpan.Behind, config.ToleranceSpan.Ahead),
|
||||
UriFormat = config.UriFormat
|
||||
};
|
||||
|
||||
options.UriFormat |= config.MinimalUri ? OtpUriFormat.Minimal : OtpUriFormat.Full;
|
||||
|
||||
foreach (KeyValuePair<string, string> pair in config.CustomProperties)
|
||||
options.CustomProperties.Add(pair.Key, pair.Value);
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>EugeneFox.SimpleOTP.DependencyInjection</PackageId>
|
||||
<Version>8.0.0.0-rc1</Version>
|
||||
<Authors>Eugene Fox</Authors>
|
||||
<Copyright>Copyright © Eugene Fox 2024</Copyright>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/XFox111/SimpleOTP.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/XFox111/SimpleOTP</PackageProjectUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageTags>
|
||||
otp;totp;hotp;authenticator;authentication;one-time;2fa;mfa;security;otpauth;services;dependency-injection;di</PackageTags>
|
||||
<Description>
|
||||
Dependency Injection implementation for SimpleOTP library. Allows to use SimpleOTP as DI
|
||||
service in your application.
|
||||
</Description>
|
||||
<PackageReleaseNotes>
|
||||
Initial release. See README.md for details.
|
||||
</PackageReleaseNotes>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\assets\icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
<None Include="..\..\README.md">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SimpleOTP\SimpleOTP.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions"
|
||||
Version="8.0.*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("SimpleOTP.Tests")]
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SimpleOTP.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a JSON converter for <see cref="OtpAlgorithm"/>.
|
||||
/// </summary>
|
||||
public class OtpAlgorithmJsonConverter : JsonConverter<OtpAlgorithm>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override OtpAlgorithm Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
new(reader.GetString() ?? OtpAlgorithm.SHA1);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, OtpAlgorithm value, JsonSerializerOptions options) =>
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SimpleOTP.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a JSON converter for <see cref="OtpCode"/>.
|
||||
/// </summary>
|
||||
public class OtpCodeJsonConverter : JsonConverter<OtpCode>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override OtpCode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
string? code = null;
|
||||
DateTimeOffset? expirationTime = null;
|
||||
|
||||
while (reader.Read())
|
||||
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||
{
|
||||
string propertyName = reader.GetString()!;
|
||||
reader.Read();
|
||||
|
||||
if (reader.TokenType != JsonTokenType.String)
|
||||
continue;
|
||||
|
||||
if (propertyName.Equals("Code", StringComparison.OrdinalIgnoreCase))
|
||||
code = reader.GetString();
|
||||
|
||||
if (propertyName.Equals("Expiring", StringComparison.OrdinalIgnoreCase) &&
|
||||
reader.TryGetDateTimeOffset(out DateTimeOffset expiring))
|
||||
expirationTime = expiring;
|
||||
}
|
||||
|
||||
if (code is null)
|
||||
throw new JsonException("Missing required property 'Code'.");
|
||||
|
||||
return new OtpCode(code, expirationTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, OtpCode value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("Code", value.ToString());
|
||||
|
||||
if (value.ExpirationTime.HasValue)
|
||||
writer.WriteString("Expiring", value.ExpirationTime.Value);
|
||||
else
|
||||
writer.WriteNull("Expiring");
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SimpleOTP.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a JSON converter for <see cref="OtpConfig"/>.
|
||||
/// </summary>
|
||||
public class OtpConfigJsonConverter : JsonConverter<OtpConfig>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override OtpConfig? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
OtpConfig.ParseUri(reader.GetString()!);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, OtpConfig value, JsonSerializerOptions options) =>
|
||||
writer.WriteStringValue(value.ToUri().AbsoluteUri);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SimpleOTP.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a JSON converter for <see cref="OtpSecret"/>.
|
||||
/// </summary>
|
||||
public class OtpSecretJsonConverter : JsonConverter<OtpSecret>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override OtpSecret Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
new(reader.GetString()!);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, OtpSecret value, JsonSerializerOptions options) =>
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
namespace SimpleOTP.Encoding;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for encoding and decoding data using the RFC 4648 Base32 standard alphabet.
|
||||
/// </summary>
|
||||
public class Base32Encoder : IEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the singleton instance of the <see cref="Base32Encoder"/> class.
|
||||
/// </summary>
|
||||
public static Base32Encoder Instance { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string Scheme => "base32";
|
||||
|
||||
/// <summary>
|
||||
/// Converts a byte array to a Base32 string representation.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array to convert.</param>
|
||||
/// <returns>The Base32 string representation of the byte array.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when parameter is null.</exception>
|
||||
public string EncodeBytes(byte[] bytes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bytes);
|
||||
|
||||
if (bytes.Length < 1)
|
||||
return string.Empty;
|
||||
|
||||
char[] outArray = new char[(int)Math.Ceiling(bytes.Length * 8 / 5d)];
|
||||
|
||||
int bitIndex = 0;
|
||||
int buffer = 0;
|
||||
int filledChars = 0;
|
||||
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
if (bitIndex >= 5)
|
||||
{
|
||||
outArray[filledChars++] = ValueToChar(buffer >> (bitIndex - 5) & 0x1F);
|
||||
bitIndex -= 5;
|
||||
}
|
||||
|
||||
outArray[filledChars++] = ValueToChar(((buffer << (5 - bitIndex)) & 0x1F) | bytes[i] >> (3 + bitIndex));
|
||||
buffer = bytes[i];
|
||||
bitIndex = 3 + bitIndex;
|
||||
}
|
||||
|
||||
// Adding trailing bits
|
||||
if (bitIndex > 0)
|
||||
outArray[filledChars] = ValueToChar(buffer << (5 - bitIndex) & 0x1F);
|
||||
|
||||
return new string(outArray);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Base32 encoded string to a byte array.
|
||||
/// </summary>
|
||||
/// <param name="inArray">The Base32 encoded string to convert.</param>
|
||||
/// <remarks>Trailing bits are ignored (e.g. AAAR will be treated as AAAQ - 0x00 0x01).</remarks>
|
||||
/// <returns>The byte array representation of the Base32 encoded string.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when parameter is null.</exception>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="inArray"/> is empty, whitespace, or contains invalid characters.</exception>
|
||||
public byte[] GetBytes(string inArray)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(inArray);
|
||||
|
||||
inArray = inArray.TrimEnd('=');
|
||||
int buffer = 0x00;
|
||||
int bitIndex = 0;
|
||||
int filledBytes = 0;
|
||||
byte[] outArray = new byte[inArray.Length * 5 / 8];
|
||||
|
||||
for (int i = 0; i < inArray.Length; i++)
|
||||
{
|
||||
int value = CharToValue(inArray[i]);
|
||||
|
||||
buffer = (buffer << 5) | value;
|
||||
bitIndex += 5;
|
||||
|
||||
if (bitIndex >= 8)
|
||||
{
|
||||
// We have enough bits to fill a byte, flushing it
|
||||
outArray[filledBytes++] = (byte)((buffer >> (bitIndex - 8)) & 0xFF);
|
||||
bitIndex -= 8;
|
||||
buffer &= 0xFF; // Trimming value to 1 byte to prevent overflow for long strings
|
||||
}
|
||||
}
|
||||
|
||||
return outArray;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Base32 character to its numeric value.
|
||||
/// </summary>
|
||||
/// <param name="c">The Base32 character to convert.</param>
|
||||
/// <returns>The numeric value of the Base32 character.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="c"/> is not a valid Base32 character.</exception>
|
||||
protected virtual int CharToValue(char c) =>
|
||||
(int)c switch
|
||||
{
|
||||
> 0x31 and < 0x38 => c - 0x18,
|
||||
> 0x40 and < 0x5B => c - 0x41,
|
||||
> 0x60 and < 0x7B => c - 0x61,
|
||||
_ => throw new ArgumentException("Character is not a Base32 character.", nameof(c)),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Converts a numeric value to its Base32 character.
|
||||
/// </summary>
|
||||
/// <param name="value">The numeric value to convert.</param>
|
||||
/// <returns>The Base32 character corresponding to the numeric value.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when <paramref name="value"/> is not a valid Base32 value.</exception>
|
||||
protected virtual char ValueToChar(int value) =>
|
||||
value switch
|
||||
{
|
||||
< 26 => (char)(value + 0x41),
|
||||
< 32 => (char)(value + 0x18),
|
||||
_ => throw new ArgumentException("Byte is not a Base32 byte.", nameof(value)),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace SimpleOTP.Encoding;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for encoding and decoding data using the RFC 4648 Base32 "Extended Hex" alphabet.
|
||||
/// </summary>
|
||||
public interface IEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the encoding scheme used by the encoder (e.g. <c>base32</c> or <c>base32hex</c>).
|
||||
/// </summary>
|
||||
public string Scheme { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts a byte array to a Base32 string representation.
|
||||
/// </summary>
|
||||
/// <param name="data">The byte array to convert.</param>
|
||||
/// <returns>The Base32 string representation of the byte array.</returns>
|
||||
public string EncodeBytes(byte[] data);
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Base32 encoded string to a byte array.
|
||||
/// </summary>
|
||||
/// <param name="data">The Base32 encoded string to convert.</param>
|
||||
/// <returns>The byte array representation of the Base32 encoded string.</returns>
|
||||
public byte[] GetBytes(string data);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace SimpleOTP.Fluent;
|
||||
|
||||
/// <summary>
|
||||
/// Class used to streamline OTP code generation on client devices.
|
||||
/// </summary>
|
||||
public static class OtpBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Use TOTP generator with optional counter period.
|
||||
/// </summary>
|
||||
/// <param name="period">Period in seconds.</param>
|
||||
/// <returns><see cref="Otp"/> instance.</returns>
|
||||
public static Otp UseTotp(int period = 30) =>
|
||||
new Totp(OtpSecret.CreateNew(), period);
|
||||
|
||||
/// <summary>
|
||||
/// Use HOTP generator with optional counter value.
|
||||
/// </summary>
|
||||
/// <param name="counter">Counter value.</param>
|
||||
/// <returns><see cref="Otp"/> instance.</returns>
|
||||
public static Otp UseHotp(long counter = 0) =>
|
||||
new Hotp(OtpSecret.CreateNew(), counter);
|
||||
|
||||
/// <summary>
|
||||
/// Creates <see cref="Otp"/> instance from <see cref="OtpConfig"/> object.
|
||||
/// </summary>
|
||||
/// <param name="config"><see cref="OtpConfig"/> object.</param>
|
||||
/// <returns><see cref="Otp"/> instance.</returns>
|
||||
public static Otp FromConfig(OtpConfig config)
|
||||
{
|
||||
Otp generator = config.Type == OtpType.Totp ?
|
||||
new Totp(config.Secret, config.Period) :
|
||||
new Hotp(config.Secret, config.Counter);
|
||||
|
||||
generator.Algorithm = config.Algorithm;
|
||||
generator.Digits = config.Digits;
|
||||
|
||||
return generator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace SimpleOTP.Fluent;
|
||||
|
||||
/// <summary>
|
||||
/// Class used to streamline OTP code configuration on client devices.
|
||||
/// </summary>
|
||||
public static class OtpConfigBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Use TOTP configuration with optional counter period.
|
||||
/// </summary>
|
||||
/// <param name="accountName">Account name.</param>
|
||||
/// <param name="period">Period in seconds.</param>
|
||||
/// <returns><see cref="OtpConfig"/> instance.</returns>
|
||||
public static OtpConfig UseTotp(string accountName, int period = 30) =>
|
||||
new(accountName)
|
||||
{
|
||||
Type = OtpType.Totp,
|
||||
Period = period
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Use HOTP configuration with optional counter.
|
||||
/// </summary>
|
||||
/// <param name="accountName">Account name.</param>
|
||||
/// <param name="counter">Counter value.</param>
|
||||
/// <returns><see cref="OtpConfig"/> instance.</returns>
|
||||
public static OtpConfig UseHotp(string accountName, long counter = 0) =>
|
||||
new(accountName)
|
||||
{
|
||||
Type = OtpType.Hotp,
|
||||
Counter = counter
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Use TOTP which satisfies Apple's specification requirements.
|
||||
/// </summary>
|
||||
/// <param name="accountName">Account name.</param>
|
||||
/// <param name="issuerName">Issuer/application/service display name.</param>
|
||||
/// <param name="issuerDomain">Issuer/application/service domain name.</param>
|
||||
/// <returns><see cref="OtpConfig"/> instance.</returns>
|
||||
public static OtpConfig UseApple(string accountName, string issuerName, string issuerDomain) =>
|
||||
new(accountName)
|
||||
{
|
||||
Type = OtpType.Totp,
|
||||
Secret = OtpSecret.CreateNew(20),
|
||||
Digits = 6,
|
||||
IssuerLabel = issuerName,
|
||||
Issuer = issuerDomain,
|
||||
Label = accountName
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
namespace SimpleOTP.Fluent;
|
||||
|
||||
/// <summary>
|
||||
/// Provides fluent API for configuring <see cref="OtpConfig"/> objects.
|
||||
/// </summary>
|
||||
public static class OtpConfigFluentExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the <see cref="OtpConfig.Label"/> property.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="OtpConfig"/> object to configure.</param>
|
||||
/// <param name="label">The label of the OTP config.</param>
|
||||
/// <returns>The configured <see cref="OtpConfig"/> object.</returns>
|
||||
public static OtpConfig WithLabel(this OtpConfig config, string label)
|
||||
{
|
||||
config.Label = label;
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="OtpConfig.Issuer"/> property.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="OtpConfig"/> object to configure.</param>
|
||||
/// <param name="issuer">The issuer of the OTP config.</param>
|
||||
/// <returns>The configured <see cref="OtpConfig"/> object.</returns>
|
||||
public static OtpConfig WithIssuer(this OtpConfig config, string? issuer)
|
||||
{
|
||||
config.Issuer = issuer;
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the issuer info, according to Apple specification.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="OtpConfig"/> object to configure.</param>
|
||||
/// <param name="displayName">The display name of the issuer.</param>
|
||||
/// <param name="domain">The domain name of the issuer.</param>
|
||||
public static OtpConfig WithAppleIssuer(this OtpConfig config, string displayName, string domain)
|
||||
{
|
||||
config.IssuerLabel = displayName;
|
||||
config.Issuer = domain;
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="OtpConfig.Secret"/> property with a new secret.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="OtpConfig"/> object to configure.</param>
|
||||
/// <param name="bytesLength">The length of the secret in bytes.</param>
|
||||
/// <returns>The configured <see cref="OtpConfig"/> object.</returns>
|
||||
public static OtpConfig WithNewSecret(this OtpConfig config, int bytesLength)
|
||||
{
|
||||
config.Secret = OtpSecret.CreateNew(bytesLength);
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="OtpConfig.Secret"/> property with specified secret.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="OtpConfig"/> object to configure.</param>
|
||||
/// <param name="secret">The secret to use.</param>
|
||||
/// <returns>The configured <see cref="OtpConfig"/> object.</returns>
|
||||
public static OtpConfig WithSecret(this OtpConfig config, OtpSecret secret)
|
||||
{
|
||||
config.Secret = secret;
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="OtpConfig.Algorithm"/> property.
|
||||
/// </summary>
|
||||
/// <remarks>Not recommended for use, since most implementations do not support custom values.</remarks>
|
||||
/// <param name="config">The <see cref="OtpConfig"/> object to configure.</param>
|
||||
/// <param name="algorithm">The algorithm to use.</param>
|
||||
/// <returns>The configured <see cref="OtpConfig"/> object.</returns>
|
||||
public static OtpConfig WithAlgorithm(this OtpConfig config, OtpAlgorithm algorithm)
|
||||
{
|
||||
config.Algorithm = algorithm;
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="OtpConfig.Digits"/> property.
|
||||
/// </summary>
|
||||
/// <remarks>Not recommended for use, since most implementations do not support custom values.</remarks>
|
||||
/// <param name="config">The <see cref="OtpConfig"/> object to configure.</param>
|
||||
/// <param name="digits">The number of digits to use.</param>
|
||||
/// <returns>The configured <see cref="OtpConfig"/> object.</returns>
|
||||
public static OtpConfig WithDigits(this OtpConfig config, int digits)
|
||||
{
|
||||
config.Digits = digits;
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom vendor-specific property to the <see cref="OtpConfig"/>.
|
||||
/// </summary>
|
||||
/// <remarks>If set, reserved keys
|
||||
/// <c>issuer, digits, counter, secret, period and algorithm</c>
|
||||
/// will be removed from the <see cref="OtpConfig.CustomProperties"/> upon it's serialization to URI.</remarks>
|
||||
/// <param name="config">The <see cref="OtpConfig"/> object to configure.</param>
|
||||
/// <param name="key">The key of the property.</param>
|
||||
/// <param name="value">The value of the property.</param>
|
||||
/// <returns>The configured <see cref="OtpConfig"/> object.</returns>
|
||||
public static OtpConfig AddCustomProperty(this OtpConfig config, string key, string value)
|
||||
{
|
||||
config.CustomProperties.Add(key, value);
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Otp"/> object from the provided <see cref="OtpConfig"/>
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="OtpConfig"/> object to use.</param>
|
||||
/// <returns>A new <see cref="Otp"/> object.</returns>
|
||||
public static Otp CreateGenerator(this OtpConfig config) =>
|
||||
OtpBuilder.FromConfig(config);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
namespace SimpleOTP.Fluent;
|
||||
|
||||
/// <summary>
|
||||
/// Provides fluent API for configuring <see cref="Otp"/> objects.
|
||||
/// </summary>
|
||||
public static class OtpFluentExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Otp"/> object from the provided <see cref="OtpConfig"/>
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="Otp"/> object to configure.</param>
|
||||
/// <param name="bytesLength">The length of the secret in bytes.</param>
|
||||
/// <returns>The configured <see cref="Otp"/> object.</returns>
|
||||
public static Otp WithNewSecret(this Otp generator, int bytesLength)
|
||||
{
|
||||
generator.Secret = OtpSecret.CreateNew(bytesLength);
|
||||
return generator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Otp"/> object from the provided <see cref="OtpSecret"/>
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="Otp"/> object to configure.</param>
|
||||
/// <param name="secret">The <see cref="OtpSecret"/> to use.</param>
|
||||
/// <returns>The configured <see cref="Otp"/> object.</returns>
|
||||
public static Otp WithSecret(this Otp generator, OtpSecret secret)
|
||||
{
|
||||
generator.Secret = secret;
|
||||
return generator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="Otp.Digits"/> property.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="Otp"/> object to configure.</param>
|
||||
/// <param name="digits">The number of digits to use in OTP codes.</param>
|
||||
/// <returns>The configured <see cref="Otp"/> object.</returns>
|
||||
public static Otp WithDigits(this Otp generator, int digits)
|
||||
{
|
||||
generator.Digits = digits;
|
||||
return generator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="Otp.Algorithm"/> property.
|
||||
/// </summary>
|
||||
/// <param name="generator">The <see cref="Otp"/> object to configure.</param>
|
||||
/// <param name="algorithm">The algorithm to use.</param>
|
||||
/// <returns>The configured <see cref="Otp"/> object.</returns>
|
||||
public static Otp WithAlgorithm(this Otp generator, OtpAlgorithm algorithm)
|
||||
{
|
||||
generator.Algorithm = algorithm;
|
||||
return generator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods for registering and retrieving <see cref="KeyedHashAlgorithm"/> providers.
|
||||
/// </summary>
|
||||
public static class HashAlgorithmProviders
|
||||
{
|
||||
private static readonly Dictionary<OtpAlgorithm, Func<KeyedHashAlgorithm>> _registeredProviders = new()
|
||||
{
|
||||
{ OtpAlgorithm.SHA1, () => new HMACSHA1() },
|
||||
{ OtpAlgorithm.SHA256, () => new HMACSHA256() },
|
||||
{ OtpAlgorithm.SHA512, () => new HMACSHA512() },
|
||||
{ OtpAlgorithm.MD5, () => new HMACMD5() }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new <see cref="KeyedHashAlgorithm"/> provider.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm to register.</param>
|
||||
public static void AddProvider<TAlgorithm>(OtpAlgorithm algorithm)
|
||||
where TAlgorithm : KeyedHashAlgorithm, new() =>
|
||||
_registeredProviders[algorithm] = () => new TAlgorithm();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="KeyedHashAlgorithm"/> provider.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm to retrieve.</param>
|
||||
/// <returns>The <see cref="KeyedHashAlgorithm"/> provider, or <c>null</c> if not found.</returns>
|
||||
public static KeyedHashAlgorithm? GetProvider(OtpAlgorithm algorithm)
|
||||
{
|
||||
if (_registeredProviders.TryGetValue(algorithm, out var provider))
|
||||
return provider();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a <see cref="KeyedHashAlgorithm"/> provider.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm to remove.</param>
|
||||
public static void RemoveProvider(OtpAlgorithm algorithm) =>
|
||||
_registeredProviders.Remove(algorithm);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a <see cref="KeyedHashAlgorithm"/> provider is registered.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm to check.</param>
|
||||
/// <returns><c>true</c> if the <see cref="KeyedHashAlgorithm"/> provider is registered; otherwise, <c>false</c>.</returns>
|
||||
public static bool IsRegistered(OtpAlgorithm algorithm) =>
|
||||
_registeredProviders.ContainsKey(algorithm);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all registered <see cref="KeyedHashAlgorithm"/> providers.
|
||||
/// </summary>
|
||||
/// <remarks>This method also clears default providers. Use with caution.</remarks>
|
||||
public static void ClearProviders() => _registeredProviders.Clear();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace SimpleOTP;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a HOTP (HMAC-based One-Time Password) generator.
|
||||
/// </summary>
|
||||
public class Hotp : Otp
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the counter value used for generating OTP codes.
|
||||
/// </summary>
|
||||
public long Counter { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Hotp"/> class
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
public Hotp(OtpSecret secret) : base(secret) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Hotp"/> class
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="counter">The counter value used for generating OTP codes.</param>
|
||||
public Hotp(OtpSecret secret, long counter) : base(secret) =>
|
||||
Counter = counter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Hotp"/> class
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="counter">The counter value used for generating OTP codes.</param>
|
||||
/// <param name="digits">The number of digits in the OTP code.</param>
|
||||
public Hotp(OtpSecret secret, long counter, int digits) : base(secret, digits) =>
|
||||
Counter = counter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Hotp"/> class
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="counter">The counter value used for generating OTP codes.</param>
|
||||
/// <param name="algorithm">The algorithm used for generating OTP codes.</param>
|
||||
public Hotp(OtpSecret secret, long counter, OtpAlgorithm algorithm) : base(secret, algorithm) =>
|
||||
Counter = counter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Hotp"/> class
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="counter">The counter value used for generating OTP codes.</param>
|
||||
/// <param name="algorithm">The algorithm used for generating OTP codes.</param>
|
||||
/// <param name="digits">The number of digits in the OTP code.</param>
|
||||
public Hotp(OtpSecret secret, long counter, OtpAlgorithm algorithm, int digits) : base(secret, algorithm, digits) =>
|
||||
Counter = counter;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current counter value.
|
||||
/// </summary>
|
||||
/// <returns>The current counter value.</returns>
|
||||
protected override long GetCounter() => Counter;
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
// TODO: Add tests
|
||||
|
||||
/// <summary>
|
||||
/// Represents an abstract class for generating and validating One-Time Passwords (OTP).
|
||||
/// </summary>
|
||||
public abstract class Otp
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret key used for generating OTPs.
|
||||
/// </summary>
|
||||
public OtpSecret Secret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the algorithm used for generating OTP codes.
|
||||
/// </summary>
|
||||
public OtpAlgorithm Algorithm { get; set; } = OtpAlgorithm.SHA1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of digits in the OTP code.
|
||||
/// </summary>
|
||||
/// <value>Default: 6. Recommended: 6-8.</value>
|
||||
public int Digits { get; set; } = 6;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Otp"/> class.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="algorithm">The algorithm used for generating OTP codes.</param>
|
||||
/// <param name="digits">The number of digits in the OTP code.</param>
|
||||
public Otp(OtpSecret secret, OtpAlgorithm algorithm, int digits) =>
|
||||
(Secret, Algorithm, Digits) = (secret, algorithm, digits);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Otp"/> class.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="algorithm">The algorithm used for generating OTP codes.</param>
|
||||
public Otp(OtpSecret secret, OtpAlgorithm algorithm) : this(secret, algorithm, 6) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Otp"/> class.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="digits">The number of digits in the OTP code.</param>
|
||||
public Otp(OtpSecret secret, int digits) : this(secret, OtpAlgorithm.SHA1, digits) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Otp"/> class.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
public Otp(OtpSecret secret) : this(secret, OtpAlgorithm.SHA1, 6) { }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
// Generate
|
||||
|
||||
/// <summary>
|
||||
/// Generates an OTP code.
|
||||
/// </summary>
|
||||
/// <returns>The generated OTP code.</returns>
|
||||
public OtpCode Generate() =>
|
||||
Generate(GetCounter());
|
||||
|
||||
/// <summary>
|
||||
/// Generates an OTP code for the specified counter value.
|
||||
/// </summary>
|
||||
/// <param name="counter">The counter value to generate the OTP code for.</param>
|
||||
/// <returns>The generated OTP code.</returns>
|
||||
public virtual OtpCode Generate(long counter) =>
|
||||
new(Compute(counter), Digits);
|
||||
|
||||
// Validate
|
||||
|
||||
/// <summary>
|
||||
/// Validates an OTP code.
|
||||
/// </summary>
|
||||
/// <param name="code">The OTP code to validate.</param>
|
||||
/// <returns><c>true</c> if the OTP code is valid; otherwise, <c>false</c>.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Implementation for the <see cref="Algorithm"/> algorithm was not found.
|
||||
/// Use <see cref="HashAlgorithmProviders.AddProvider(OtpAlgorithm)"/> to register an implementation.
|
||||
/// </exception>
|
||||
public bool Validate(OtpCode code) =>
|
||||
Validate(code, (1, 1));
|
||||
|
||||
/// <summary>
|
||||
/// Validates an OTP code with tolerance.
|
||||
/// </summary>
|
||||
/// <param name="code">The OTP code to validate.</param>
|
||||
/// <param name="tolerance">The tolerance span for code validation.</param>
|
||||
/// <returns><c>true</c> if the OTP code is valid; otherwise, <c>false</c>.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Implementation for the <see cref="Algorithm"/> algorithm was not found.
|
||||
/// Use <see cref="HashAlgorithmProviders.AddProvider(OtpAlgorithm)"/> to register an implementation.
|
||||
/// </exception>
|
||||
public bool Validate(OtpCode code, ToleranceSpan tolerance) =>
|
||||
Validate(code, tolerance, out _);
|
||||
|
||||
/// <summary>
|
||||
/// Validates an OTP code with tolerance and returns the resynchronization value.
|
||||
/// </summary>
|
||||
/// <param name="code">The OTP code to validate.</param>
|
||||
/// <param name="tolerance">The tolerance span for code validation.</param>
|
||||
/// <param name="resyncValue">The resynchronization value. Indicates how much given OTP code is ahead or behind the current counter value.</param>
|
||||
/// <returns><c>true</c> if the OTP code is valid; otherwise, <c>false</c>.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Implementation for the <see cref="Algorithm"/> algorithm was not found.
|
||||
/// Use <see cref="HashAlgorithmProviders.AddProvider(OtpAlgorithm)"/> to register an implementation.
|
||||
/// </exception>
|
||||
public bool Validate(OtpCode code, ToleranceSpan tolerance, out int resyncValue) =>
|
||||
Validate(code, tolerance, GetCounter(), out resyncValue);
|
||||
|
||||
/// <summary>
|
||||
/// Validates an OTP code with tolerance and base counter value, and returns the resynchronization value.
|
||||
/// </summary>
|
||||
/// <param name="code">The OTP code to validate.</param>
|
||||
/// <param name="tolerance">The tolerance span for code validation.</param>
|
||||
/// <param name="baseCounter">The base counter value.</param>
|
||||
/// <param name="resyncValue">The resynchronization value. Indicates how much given OTP code is ahead or behind the current counter value.</param>
|
||||
/// <returns><c>true</c> if the OTP code is valid; otherwise, <c>false</c>.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Implementation for the <see cref="Algorithm"/> algorithm was not found.
|
||||
/// Use <see cref="HashAlgorithmProviders.AddProvider(OtpAlgorithm)"/> to register an implementation.
|
||||
/// </exception>
|
||||
public bool Validate(OtpCode code, ToleranceSpan tolerance, long baseCounter, out int resyncValue)
|
||||
{
|
||||
resyncValue = 0;
|
||||
|
||||
using KeyedHashAlgorithm? hashAlgorithm = HashAlgorithmProviders.GetProvider(Algorithm) ??
|
||||
throw new InvalidOperationException($"Implementation for the \"{Algorithm}\" algorithm was not found.");
|
||||
|
||||
for (int i = -tolerance.Behind; i <= tolerance.Ahead; i++)
|
||||
if (code == Compute(baseCounter + i, hashAlgorithm).ToString($"D{Digits}"))
|
||||
{
|
||||
resyncValue = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current counter value.
|
||||
/// </summary>
|
||||
/// <returns>The current counter value.</returns>
|
||||
protected abstract long GetCounter();
|
||||
|
||||
/// <summary>
|
||||
/// Computes the OTP code for the specified counter value.
|
||||
/// </summary>
|
||||
/// <param name="counter">The counter value to compute the OTP code for.</param>
|
||||
/// <returns>The OTP code for the specified counter value.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Implementation for the <see cref="Algorithm"/> algorithm was not found.
|
||||
/// Use <see cref="HashAlgorithmProviders.AddProvider(OtpAlgorithm)"/> to register an implementation.
|
||||
/// </exception>
|
||||
protected int Compute(long counter)
|
||||
{
|
||||
using KeyedHashAlgorithm? hashAlgorithm = HashAlgorithmProviders.GetProvider(Algorithm) ??
|
||||
throw new InvalidOperationException($"Implementation for the \"{Algorithm}\" algorithm was not found.");
|
||||
|
||||
return Compute(counter, hashAlgorithm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the OTP code for the specified counter value using provided hash algorithm.
|
||||
/// </summary>
|
||||
/// <param name="counter">The counter value to compute the OTP code for.</param>
|
||||
/// <param name="hashAlgorithm">The hash algorithm to use for computing the OTP code.</param>
|
||||
/// <remarks>You need to dispose of the <paramref name="hashAlgorithm"/> object yourself when you are done using it.</remarks>
|
||||
/// <returns>The OTP code for the specified counter value.</returns>
|
||||
protected virtual int Compute(long counter, KeyedHashAlgorithm hashAlgorithm)
|
||||
{
|
||||
byte[] counterBytes = BitConverter.GetBytes(counter);
|
||||
|
||||
// "The HOTP values generated by the HOTP generator are treated as big endian."
|
||||
// https://datatracker.ietf.org/doc/html/rfc4226#section-5.2
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(counterBytes);
|
||||
|
||||
hashAlgorithm.Key = Secret;
|
||||
|
||||
byte[] hash = hashAlgorithm.ComputeHash(counterBytes);
|
||||
|
||||
// Converting hash to n-digits value
|
||||
// See RFC4226 Section 5.4 for more details
|
||||
// https://datatracker.ietf.org/doc/html/rfc4226#section-5.4
|
||||
int offset = hash[^1] & 0x0F;
|
||||
|
||||
int value =
|
||||
(hash[offset + 0] & 0x7F) << 24 | // Result value should be a 31-bit integer, hence the 0x7F (0111 1111)
|
||||
(hash[offset + 1] & 0xFF) << 16 |
|
||||
(hash[offset + 2] & 0xFF) << 8 |
|
||||
(hash[offset + 3] & 0xFF) << 0;
|
||||
|
||||
return value % (int)Math.Pow(10, Digits);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
using System.Xml.Serialization;
|
||||
using SimpleOTP.Converters;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the hashing algorithm used for One-Time Passwords.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[JsonConverter(typeof(OtpAlgorithmJsonConverter))]
|
||||
public readonly partial struct OtpAlgorithm : IEquatable<OtpAlgorithm>, IEquatable<string>, IXmlSerializable
|
||||
{
|
||||
/// <summary>
|
||||
/// The HMAC-SHA1 hashing algorithm.
|
||||
/// </summary>
|
||||
public static OtpAlgorithm SHA1 { get; } = new("SHA1");
|
||||
|
||||
/// <summary>
|
||||
/// The HMAC-SHA256 hashing algorithm.
|
||||
/// </summary>
|
||||
public static OtpAlgorithm SHA256 { get; } = new("SHA256");
|
||||
|
||||
/// <summary>
|
||||
/// The HMAC-SHA512 hashing algorithm.
|
||||
/// </summary>
|
||||
public static OtpAlgorithm SHA512 { get; } = new("SHA512");
|
||||
|
||||
/// <summary>
|
||||
/// The HMAC-MD5 hashing algorithm.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not a standard algorithm, but it is defined by IIJ specification and recognized by default.<br />
|
||||
/// <a href="https://www1.auth.iij.jp/smartkey/en/uri_v1.html">Internet Initiative Japan. URI format</a>
|
||||
/// </remarks>
|
||||
public static OtpAlgorithm MD5 { get; } = new("MD5");
|
||||
|
||||
private readonly string _value;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpAlgorithm"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">The algorithm to use.</param>
|
||||
/// <exception cref="ArgumentException">Thrown if <paramref name="value"/> is empty or whitespace.</exception>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is <see langword="null"/>.</exception>
|
||||
public OtpAlgorithm(string value)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(value);
|
||||
|
||||
if (StandardAlgorithmsRegex().IsMatch(value))
|
||||
_value = StandardAlgorithmsRegex().Match(value).Value.ToUpperInvariant();
|
||||
else
|
||||
_value = value.ToUpperInvariant();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(OtpAlgorithm other) =>
|
||||
_value == other._value;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(string? other)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(other))
|
||||
return _value is null;
|
||||
if (_value is null)
|
||||
return false;
|
||||
|
||||
return Equals(new OtpAlgorithm(other));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is OtpAlgorithm algorithm && Equals(algorithm);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="OtpAlgorithm"/> is standard HMAC SHA algorithm (SHA-1, SHA-256 or SHA-512).
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the specified <see cref="OtpAlgorithm"/> is standard; otherwise, <see langword="false"/>.</returns>
|
||||
public bool IsStandard() =>
|
||||
IsStandard(_value);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => _value.GetHashCode();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the <see cref="OtpAlgorithm"/> struct.
|
||||
/// </summary>
|
||||
/// <returns>The string representation of the <see cref="OtpAlgorithm"/> struct.</returns>
|
||||
public override string ToString() => _value;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public XmlSchema? GetSchema() => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ReadXml(XmlReader reader)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
throw new XmlException("Invalid XML element.");
|
||||
|
||||
string algorithm = reader.ReadElementContentAsString();
|
||||
|
||||
#pragma warning disable CS9195 // Argument should be passed with the in keyword
|
||||
Unsafe.AsRef(this) = new OtpAlgorithm(algorithm);
|
||||
#pragma warning restore CS9195 // Argument should be passed with the in keyword
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void WriteXml(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("standard", IsStandard().ToString().ToLowerInvariant());
|
||||
writer.WriteString(_value);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static bool operator ==(OtpAlgorithm left, OtpAlgorithm right) => left.Equals(right);
|
||||
public static bool operator ==(string left, OtpAlgorithm right) => right.Equals(left);
|
||||
public static bool operator ==(OtpAlgorithm left, string right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(OtpAlgorithm left, OtpAlgorithm right) => !(left == right);
|
||||
public static bool operator !=(string left, OtpAlgorithm right) => !(left == right);
|
||||
public static bool operator !=(OtpAlgorithm left, string right) => !(left == right);
|
||||
|
||||
public static implicit operator string(OtpAlgorithm algorithm) => algorithm._value;
|
||||
public static explicit operator OtpAlgorithm(string value) => new(value);
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="OtpAlgorithm"/> is standard HMAC SHA algorithm (SHA-1, SHA-256 or SHA-512).
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm to check.</param>
|
||||
/// <returns><see langword="true"/> if the specified <see cref="OtpAlgorithm"/> is standard; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsStandard(string algorithm) =>
|
||||
StandardAlgorithmsRegex().IsMatch(algorithm);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="OtpAlgorithm"/> is standard HMAC SHA algorithm (SHA-1, SHA-256 or SHA-512).
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The algorithm to check.</param>
|
||||
/// <returns><see langword="true"/> if the specified <see cref="OtpAlgorithm"/> is standard; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool IsStandard(OtpAlgorithm algorithm) =>
|
||||
IsStandard(algorithm._value);
|
||||
|
||||
[GeneratedRegex(@"(?<=Hmac)?SHA(1|256|512)", RegexOptions.IgnoreCase, "")]
|
||||
private static partial Regex StandardAlgorithmsRegex();
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
namespace SimpleOTP;
|
||||
|
||||
// THIS IS THE BASE OF A PARTIAL STRUCT
|
||||
// List of files
|
||||
// - OtpCode.Base.cs - Base file
|
||||
// - OtpCode.Static.cs - Static members
|
||||
// - OtpCode.Serialization.cs - JSON/XML serialization members and attributes
|
||||
|
||||
/// <summary>
|
||||
/// Represents a one-time password (OTP) code.
|
||||
/// </summary>
|
||||
public readonly partial struct OtpCode : IEquatable<OtpCode>, IEquatable<string>
|
||||
{
|
||||
private readonly int _value;
|
||||
private readonly int _digits;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the OTP code can expire (<c>true</c> for TOTP, <c>false</c> for HOTP).
|
||||
/// </summary>
|
||||
public readonly bool CanExpire => ExpirationTime is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expiration time of the OTP code (TOTP only).
|
||||
/// </summary>
|
||||
public readonly DateTimeOffset? ExpirationTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpCode"/> struct with the specified value with no expiration time.
|
||||
/// </summary>
|
||||
/// <param name="code">The value of the OTP code.</param>
|
||||
/// <param name="digits">The number of digits in the OTP code.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="code"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="code"/> is not a valid numeric code.</exception>
|
||||
public OtpCode(int code, int digits) : this(code, digits, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpCode"/> struct with the specified value and the expiration time.
|
||||
/// </summary>
|
||||
/// <param name="code">The value of the OTP code.</param>
|
||||
/// <param name="digits">The number of digits in the OTP code.</param>
|
||||
/// <param name="expirationTime">The expiration time of the OTP code (TOTP only).</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="code"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="code"/> is not a valid numeric code.</exception>
|
||||
public OtpCode(int code, int digits, DateTimeOffset? expirationTime = null)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(code);
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(digits);
|
||||
|
||||
_value = code;
|
||||
_digits = digits;
|
||||
ExpirationTime = expirationTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpCode"/> struct with the specified value with no expiration time.
|
||||
/// </summary>
|
||||
/// <param name="code">The value of the OTP code.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="code"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="code"/> is not a valid numeric code.</exception>
|
||||
public OtpCode(string code) : this(code, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpCode"/> struct with the specified value and the expiration time.
|
||||
/// </summary>
|
||||
/// <param name="code">The value of the OTP code.</param>
|
||||
/// <param name="expirationTime">The expiration time of the OTP code (TOTP only).</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="code"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="code"/> is not a valid numeric code.</exception>
|
||||
public OtpCode(string code, DateTimeOffset? expirationTime = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(code);
|
||||
|
||||
if (!int.TryParse(code, out var value))
|
||||
throw new ArgumentException($"'{code}' is not a valid numeric code.", nameof(code));
|
||||
|
||||
_value = value;
|
||||
_digits = code.Length;
|
||||
ExpirationTime = expirationTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the OTP code.
|
||||
/// </summary>
|
||||
/// <returns>A string representation of the OTP code.</returns>
|
||||
public override readonly string ToString() =>
|
||||
_value.ToString($"D{_digits}");
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the OTP code.
|
||||
/// </summary>
|
||||
/// <param name="format">The format to use.</param>
|
||||
/// <returns>The string representation of the OTP code.</returns>
|
||||
public readonly string ToString(string? format) =>
|
||||
_value.ToString(format);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(OtpCode other) =>
|
||||
ToString() == other.ToString();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is OtpCode code && Equals(code);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(string? other) =>
|
||||
ToString() == other;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() =>
|
||||
_value.GetHashCode();
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
using System.Xml.Serialization;
|
||||
using SimpleOTP.Converters;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
// THIS IS THE SECTION OF A PARTIAL STRUCT
|
||||
// Description: Section of OtpCode struct that holds JSON/XML serialization members and attributes
|
||||
// Base file: OtpCode.Base.cs
|
||||
|
||||
[Serializable]
|
||||
[JsonConverter(typeof(OtpCodeJsonConverter))]
|
||||
public readonly partial struct OtpCode : IXmlSerializable
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public XmlSchema? GetSchema() => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ReadXml(XmlReader reader)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
throw new XmlException("Invalid XML element.");
|
||||
|
||||
DateTimeOffset? expirationTime = null;
|
||||
|
||||
if (reader.HasAttributes && reader.MoveToAttribute("expiring"))
|
||||
expirationTime = DateTimeOffset.ParseExact(reader.ReadContentAsString(), "O", null);
|
||||
|
||||
reader.MoveToContent();
|
||||
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
throw new XmlException("Invalid XML content.");
|
||||
|
||||
string code = reader.ReadElementContentAsString();
|
||||
|
||||
#pragma warning disable CS9195 // Argument should be passed with the in keyword
|
||||
Unsafe.AsRef(this) = new OtpCode(code, expirationTime);
|
||||
#pragma warning restore CS9195 // Argument should be passed with the in keyword
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void WriteXml(XmlWriter writer)
|
||||
{
|
||||
if (ExpirationTime.HasValue)
|
||||
writer.WriteAttributeString("expiring", ExpirationTime.Value.ToString("O"));
|
||||
|
||||
writer.WriteString(ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace SimpleOTP;
|
||||
|
||||
// THIS IS THE SECTION OF A PARTIAL STRUCT
|
||||
// Description: Section of OtpCode struct that holds static members
|
||||
// Base file: OtpCode.Base.cs
|
||||
|
||||
public readonly partial struct OtpCode
|
||||
{
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static implicit operator string(OtpCode code) => code.ToString();
|
||||
public static implicit operator OtpCode(string code) => new(code);
|
||||
|
||||
public static bool operator ==(OtpCode left, OtpCode right) => left.Equals(right);
|
||||
public static bool operator ==(string left, OtpCode right) => right.Equals(left);
|
||||
public static bool operator ==(OtpCode left, string right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(OtpCode left, OtpCode right) => !(left == right);
|
||||
public static bool operator !=(string left, OtpCode right) => !(left == right);
|
||||
public static bool operator !=(OtpCode left, string right) => !(left == right);
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
/// <summary>
|
||||
/// Parses the specified <see cref="string"/> into an <see cref="OtpCode"/> object.
|
||||
/// </summary>
|
||||
/// <param name="code">The string to parse.</param>
|
||||
/// <returns>An <see cref="OtpCode"/> object.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="code"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="code"/> is not a valid numeric code.</exception>
|
||||
public static OtpCode Parse(string code) =>
|
||||
new(code);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the specified <see cref="string"/> into an <see cref="OtpCode"/> object.
|
||||
/// </summary>
|
||||
/// <param name="code">The string to parse.</param>
|
||||
/// <param name="result">The parsed <see cref="OtpCode"/> object.</param>
|
||||
/// <returns><see langword="true"/> if <paramref name="code"/> was parsed successfully; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool TryParse(string code, out OtpCode result)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = new(code);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
// THIS IS THE BASE OF A PARTIAL CLASS
|
||||
// List of files
|
||||
// - OtpConfig.Base.cs - Base file
|
||||
// - OtpConfig.Constructors.cs - Instance constructors
|
||||
// - OtpConfig.Methods.cs - Instance methods and serialization
|
||||
// - OtpConfig.Static.cs - Static members
|
||||
|
||||
/// <summary>
|
||||
/// Represents the configuration for a One-Time Password (OTP).
|
||||
/// </summary>
|
||||
public partial record class OtpConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the OTP.
|
||||
/// </summary>
|
||||
/// <value>Default is: <see cref="OtpType.Totp"/></value>
|
||||
/// <remarks>
|
||||
/// <a href="https://www.ietf.org/archive/id/draft-linuxgemini-otpauth-uri-00.html#section-3.1">Internet-Draft</a>.<br />
|
||||
/// <b>IMPORTANT:</b> Some authenticators do not support <see cref="OtpType.Hotp"/>.
|
||||
/// </remarks>
|
||||
public OtpType Type { get; set; } = OtpType.Totp;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the issuer label prefix of the OTP.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item>Not recommended for use in most cases.</item>
|
||||
/// <item>Most authenticators do not support this prefix and mess with the <see cref="Label"/> string.</item>
|
||||
/// <item>Required if you intend to use <see cref="OtpUriFormat.Apple"/>. Use this prefix to set the issuer display name.</item>
|
||||
/// </list>
|
||||
/// <a href="https://www.ietf.org/archive/id/draft-linuxgemini-otpauth-uri-00.html#section-3.2">Internet-Draft</a>.
|
||||
/// </remarks>
|
||||
public string? IssuerLabel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the label of the OTP.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <a href="https://www.ietf.org/archive/id/draft-linuxgemini-otpauth-uri-00.html#section-3.2">Internet-Draft</a>.
|
||||
/// </remarks>
|
||||
public string Label { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the secret of the OTP.
|
||||
/// </summary>
|
||||
/// <value>Default: 160-bit key. Minimal recommended: 128 bits</value>
|
||||
/// <remarks>
|
||||
/// <a href="https://www.ietf.org/archive/id/draft-linuxgemini-otpauth-uri-00.html#section-3.3.1">Internet-Draft</a>
|
||||
/// </remarks>
|
||||
public OtpSecret Secret { get; set; } = OtpSecret.CreateNew();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hashing algorithm of the OTP.
|
||||
/// </summary>
|
||||
/// <value>Default: <see cref="OtpAlgorithm.SHA1"/></value>
|
||||
/// <remarks>
|
||||
/// <a href="https://www.ietf.org/archive/id/draft-linuxgemini-otpauth-uri-00.html#section-3.3.2">Internet-Draft</a><br />
|
||||
/// <b>IMPORTANT:</b> Some authenticators do not support algorithms other than <see cref="OtpAlgorithm.SHA1"/>.
|
||||
/// </remarks>
|
||||
public OtpAlgorithm Algorithm { get; set; } = OtpAlgorithm.SHA1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the issuer of the OTP. Optional.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item>Use this property instead of <see cref="IssuerLabel"/>.</item>
|
||||
/// <item>Required if you intend to use <see cref="OtpUriFormat.Apple"/>. Use this property to set the issuer domain name.</item>
|
||||
/// </list>
|
||||
/// <a href="https://www.ietf.org/archive/id/draft-linuxgemini-otpauth-uri-00.html#section-3.3.6">Internet-Draft</a>
|
||||
/// </remarks>
|
||||
public string? Issuer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of digits of the OTP codes.
|
||||
/// </summary>
|
||||
/// <value>Default: 6. Recommended: 6 or 8</value>
|
||||
/// <remarks>
|
||||
/// <a href="https://www.ietf.org/archive/id/draft-linuxgemini-otpauth-uri-00.html#section-3.3.3">Internet-Draft</a><br />
|
||||
/// <b>IMPORTANT:</b> Some authenticators do not support digits other than 6.
|
||||
/// </remarks>
|
||||
public int Digits { get; set; } = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the counter of the OTP. Required for <see cref="OtpType.Hotp"/>. Ignored for <see cref="OtpType.Totp"/>.
|
||||
/// </summary>
|
||||
/// <value>Default: 0</value>
|
||||
/// <remarks>
|
||||
/// <a href="https://www.ietf.org/archive/id/draft-linuxgemini-otpauth-uri-00.html#section-3.3.4">Internet-Draft</a><br />
|
||||
/// <b>IMPORTANT:</b> Some authenticators do not support <see cref="OtpType.Hotp"/>.
|
||||
/// </remarks>
|
||||
public long Counter { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the period of the OTP in seconds. Optional for <see cref="OtpType.Totp"/>. Ignored for <see cref="OtpType.Hotp"/>.
|
||||
/// </summary>
|
||||
/// <value>Default: 30</value>
|
||||
/// <remarks>
|
||||
/// <a href="https://www.ietf.org/archive/id/draft-linuxgemini-otpauth-uri-00.html#section-3.3.5">Internet-Draft</a><br />
|
||||
/// <b>IMPORTANT:</b> Some authenticators support only periods of 30 seconds.
|
||||
/// </remarks>
|
||||
public int Period { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the custom vendor-specified properties of the current OTP configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If set, reserved keys
|
||||
/// <c>issuer, digits, counter, secret, period and algorithm</c>
|
||||
/// will be removed from the <see cref="CustomProperties"/> upon it's serialization to URI.<br />
|
||||
/// <a href="https://www.ietf.org/archive/id/draft-linuxgemini-otpauth-uri-00.html#section-3.3.7">Internet-Draft</a>
|
||||
/// </remarks>
|
||||
public NameValueCollection CustomProperties { get; } = [];
|
||||
|
||||
// Reserved keys, which are to be removed for CustomProperties
|
||||
private static readonly string[] _reservedKeys =
|
||||
[
|
||||
nameof(Issuer),
|
||||
nameof(Digits),
|
||||
nameof(Counter),
|
||||
nameof(Secret),
|
||||
nameof(Period),
|
||||
nameof(Algorithm)
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using System.Web;
|
||||
using SimpleOTP.Encoding;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
// THIS IS THE SECTION OF A PARTIAL CLASS
|
||||
// Description: Section of OtpConfig struct that holds instance constructors
|
||||
// Base file: OtpConfig.Base.cs
|
||||
|
||||
public partial record class OtpConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpConfig"/> class.
|
||||
/// </summary>
|
||||
/// <param name="label">The label of the OTP config.</param>
|
||||
public OtpConfig(string label) =>
|
||||
Label = label;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpConfig"/> class.
|
||||
/// </summary>
|
||||
/// <param name="label">The label of the OTP config.</param>
|
||||
/// <param name="issuer">The issuer of the OTP config.</param>
|
||||
public OtpConfig(string label, string issuer) =>
|
||||
(Label, Issuer) = (label, issuer);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpConfig"/> class.
|
||||
/// </summary>
|
||||
/// <param name="label">The label of the OTP config.</param>
|
||||
/// <param name="secret">The secret of the OTP config.</param>
|
||||
public OtpConfig(string label, OtpSecret secret) =>
|
||||
(Label, Secret) = (label, secret);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpConfig"/> class.
|
||||
/// </summary>
|
||||
/// <param name="label">The label of the OTP config.</param>
|
||||
/// <param name="issuer">The issuer of the OTP config.</param>
|
||||
/// <param name="secret">The secret of the OTP config.</param>
|
||||
public OtpConfig(string label, string issuer, OtpSecret secret) =>
|
||||
(Label, Issuer, Secret) = (label, issuer, secret);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpConfig"/> class.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the OTP config.</param>
|
||||
/// <exception cref="ArgumentException">Provided URI is not valid (missing required values or has invalid required values).</exception>
|
||||
public OtpConfig(Uri uri) : this(uri, OtpSecret.DefaultEncoder) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpConfig"/> class.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the OTP config.</param>
|
||||
/// <param name="encoder">The encoder used to decode the secret.</param>
|
||||
/// <exception cref="ArgumentException">Provided URI is not valid (missing required values or has invalid required values).</exception>
|
||||
public OtpConfig(Uri uri, IEncoder encoder)
|
||||
{
|
||||
if (uri.Scheme != "otpauth" && uri.Scheme != "apple-otpauth")
|
||||
throw new ArgumentException("Invalid URI scheme. Expected 'otpauth' or 'apple-otpauth'.");
|
||||
|
||||
Type = uri.Host.ToLowerInvariant() switch
|
||||
{
|
||||
"totp" => OtpType.Totp,
|
||||
"hotp" => OtpType.Hotp,
|
||||
_ => throw new ArgumentException("Invalid OTP type. Expected 'totp' or 'hotp'.")
|
||||
};
|
||||
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
Secret = OtpSecret.Parse(query[nameof(Secret)] ?? throw new ArgumentException("Secret is required."), encoder);
|
||||
|
||||
string label = WebUtility.UrlDecode(uri.Segments[^1]);
|
||||
string[] labelParts = label.Split(':', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
Label = labelParts.Last();
|
||||
|
||||
if (labelParts.Length > 1)
|
||||
IssuerLabel = labelParts[0];
|
||||
|
||||
if (long.TryParse(query[nameof(Counter)], out long counter) || Type != OtpType.Hotp)
|
||||
Counter = counter;
|
||||
else
|
||||
throw new ArgumentException("Counter is required for HOTP algorithm.");
|
||||
|
||||
if (query.Get(nameof(Issuer)) is string issuer)
|
||||
Issuer = issuer;
|
||||
|
||||
if (query.Get(nameof(Algorithm)) is string algorithm && !string.IsNullOrWhiteSpace(algorithm))
|
||||
Algorithm = (OtpAlgorithm)algorithm;
|
||||
|
||||
if (int.TryParse(query[nameof(Period)], out int period))
|
||||
Period = period;
|
||||
|
||||
if (int.TryParse(query[nameof(Digits)], out int digits))
|
||||
Digits = digits;
|
||||
|
||||
foreach (string key in _reservedKeys)
|
||||
query.Remove(key);
|
||||
|
||||
CustomProperties.Add(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Web;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
using System.Xml.Serialization;
|
||||
using SimpleOTP.Converters;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
// THIS IS THE SECTION OF A PARTIAL CLASS
|
||||
// Description: Section of OtpConfig struct that holds instance methods
|
||||
// Base file: OtpConfig.Base.cs
|
||||
|
||||
[Serializable]
|
||||
[JsonConverter(typeof(OtpConfigJsonConverter))]
|
||||
public partial record class OtpConfig : IXmlSerializable
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the current <see cref="OtpConfig"/> object to a <see cref="Uri"/> object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Uses minimal Google specified formatting (<see cref="OtpUriFormat.Minimal"/> | <see cref="OtpUriFormat.Google"/>).
|
||||
/// </remarks>
|
||||
/// <returns>A <see cref="Uri"/> object representing the current <see cref="OtpConfig"/> object.</returns>
|
||||
public Uri ToUri() =>
|
||||
ToUri(OtpUriFormat.Minimal | OtpUriFormat.Google);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the current <see cref="OtpConfig"/> object to a <see cref="Uri"/> object.
|
||||
/// </summary>
|
||||
/// <param name="format">A bitwise combination of the enumeration values that specifies the format of the URI.</param>
|
||||
/// <returns>A <see cref="Uri"/> object representing the current <see cref="OtpConfig"/> object.</returns>
|
||||
public Uri ToUri(OtpUriFormat format)
|
||||
{
|
||||
string scheme = format.HasFlag(OtpUriFormat.Apple) ? "apple-otpauth" : "otpauth";
|
||||
string label = HttpUtility.UrlEncode(Label).Replace("+", "%20");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(IssuerLabel))
|
||||
label = $"{HttpUtility.UrlEncode(IssuerLabel).Replace("+", "%20")}:{label}";
|
||||
|
||||
UriBuilder uri = new(scheme, Type.ToString().ToLowerInvariant(), -1, label);
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
query["secret"] = Secret.ToString();
|
||||
|
||||
if (Type == OtpType.Hotp)
|
||||
query["counter"] = Counter.ToString();
|
||||
|
||||
if (Issuer is not null)
|
||||
query["issuer"] = Issuer;
|
||||
|
||||
if (format.HasFlag(OtpUriFormat.Full) || !Algorithm.Equals(OtpAlgorithm.SHA1))
|
||||
{
|
||||
if (format.HasFlag(OtpUriFormat.IBM) && Algorithm.IsStandard())
|
||||
query["algorithm"] = "Hmac" + Algorithm;
|
||||
else
|
||||
query["algorithm"] = Algorithm;
|
||||
}
|
||||
|
||||
if (format.HasFlag(OtpUriFormat.Full) || Digits != 6)
|
||||
query["digits"] = Digits.ToString();
|
||||
|
||||
if (format.HasFlag(OtpUriFormat.Full) || Period != 30)
|
||||
query["period"] = Period.ToString();
|
||||
|
||||
foreach (string key in _reservedKeys)
|
||||
CustomProperties.Remove(key);
|
||||
|
||||
query.Add(CustomProperties);
|
||||
|
||||
uri.Query = query.ToString();
|
||||
return uri.Uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if the specified <see cref="OtpConfig"/> object is valid.
|
||||
/// </summary>
|
||||
/// <param name="error">The error message returned if the <see cref="OtpConfig"/> object is invalid.</param>
|
||||
/// <param name="format">The <see cref="OtpUriFormat"/> to use for validation.</param>
|
||||
/// <returns><c>true</c> if the conversion succeeded; otherwise, <c>false</c>.</returns>
|
||||
public bool IsValid([NotNullWhen(false)] out string? error, OtpUriFormat format = OtpUriFormat.Google) =>
|
||||
Validate(this, out error, format);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() =>
|
||||
ToUri().AbsoluteUri;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public XmlSchema? GetSchema() => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ReadXml(XmlReader reader)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
throw new XmlException("Invalid XML element.");
|
||||
|
||||
#pragma warning disable CS9193 // Argument should be a variable because it is passed to a 'ref readonly' parameter
|
||||
Unsafe.AsRef(this) = ParseUri(reader.ReadElementContentAsString());
|
||||
#pragma warning restore CS9193 // Argument should be a variable because it is passed to a 'ref readonly' parameter
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void WriteXml(XmlWriter writer)
|
||||
{
|
||||
writer.WriteString(ToUri().AbsoluteUri);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SimpleOTP.Encoding;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
// THIS IS THE SECTION OF A PARTIAL CLASS
|
||||
// Description: Section of OtpConfig struct that holds static members
|
||||
// Base file: OtpConfig.Base.cs
|
||||
|
||||
public partial record class OtpConfig
|
||||
{
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static implicit operator Uri(OtpConfig config) => config.ToUri();
|
||||
public static implicit operator string(OtpConfig config) => config.ToString();
|
||||
|
||||
public static explicit operator OtpConfig(Uri uri) => ParseUri(uri);
|
||||
public static explicit operator OtpConfig(string uri) => ParseUri(uri);
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
/// <summary>
|
||||
/// Parses the specified URI into an <see cref="OtpConfig"/> object.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to parse.</param>
|
||||
/// <returns>An <see cref="OtpConfig"/> object parsed from the specified URI.</returns>
|
||||
/// <exception cref="ArgumentException">Provided URI is not valid (missing required values or has invalid required values).</exception>
|
||||
public static OtpConfig ParseUri(Uri uri) =>
|
||||
new(uri);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpConfig"/> class.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI of the OTP.</param>
|
||||
/// <param name="encoder">The encoder used to decode the secret.</param>
|
||||
/// <exception cref="ArgumentException">Provided URI is not valid (missing required values or has invalid required values).</exception>
|
||||
public static OtpConfig ParseUri(Uri uri, IEncoder encoder) =>
|
||||
new(uri, encoder);
|
||||
|
||||
/// <summary>
|
||||
/// Parses the specified URI into an <see cref="OtpConfig"/> object.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to parse.</param>
|
||||
/// <returns>An <see cref="OtpConfig"/> object parsed from the specified URI.</returns>
|
||||
/// <exception cref="ArgumentException">Provided URI is not valid (missing required values or has invalid required values).</exception>
|
||||
/// <exception cref="UriFormatException">Provided URI is not valid (missing required values or has invalid required values).</exception>
|
||||
public static OtpConfig ParseUri(string uri) =>
|
||||
new(new Uri(uri));
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the specified URI into an <see cref="OtpConfig"/> object.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to parse.</param>
|
||||
/// <param name="config">When this method returns, contains the <see cref="OtpConfig"/> object parsed from the specified URI, if the conversion succeeded, or <c>null</c> if the conversion failed.</param>
|
||||
/// <returns><c>true</c> if the conversion succeeded; otherwise, <c>false</c>.</returns>
|
||||
public static bool TryParseUri(Uri uri, [NotNullWhen(true)] out OtpConfig? config)
|
||||
{
|
||||
try
|
||||
{
|
||||
config = new(uri);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
config = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the specified URI into an <see cref="OtpConfig"/> object.
|
||||
/// </summary>
|
||||
/// <param name="uri">The URI to parse.</param>
|
||||
/// <param name="config">When this method returns, contains the <see cref="OtpConfig"/> object parsed from the specified URI, if the conversion succeeded, or <c>null</c> if the conversion failed.</param>
|
||||
/// <returns><c>true</c> if the conversion succeeded; otherwise, <c>false</c>.</returns>
|
||||
public static bool TryParseUri(string uri, [NotNullWhen(true)] out OtpConfig? config) =>
|
||||
TryParseUri(new Uri(uri), out config);
|
||||
|
||||
/// <summary>
|
||||
/// Returns if the specified <see cref="OtpConfig"/> object is valid.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="OtpConfig"/> object to validate.</param>
|
||||
/// <param name="error">The error message returned if the <see cref="OtpConfig"/> object is invalid.</param>
|
||||
/// <param name="format">The <see cref="OtpUriFormat"/> to use for validation.</param>
|
||||
/// <remarks>The <paramref name="format"/> should contain at least one vendor-specific format.</remarks>
|
||||
/// <returns><c>true</c> if the conversion succeeded; otherwise, <c>false</c>.</returns>
|
||||
public static bool Validate(OtpConfig config, [NotNullWhen(false)] out string? error, OtpUriFormat format = OtpUriFormat.Google)
|
||||
{
|
||||
List<string> errors = [];
|
||||
|
||||
// Check label presence
|
||||
if (string.IsNullOrWhiteSpace(config.Label))
|
||||
errors.Add($"- '{nameof(config.Label)}' is required and must be a display name for the account.");
|
||||
|
||||
if ((format.HasFlag(OtpUriFormat.Apple) || format.HasFlag(OtpUriFormat.IIJ)) && config.Type != OtpType.Totp)
|
||||
errors.Add($"- '{nameof(config.Type)}' must be '{OtpType.Totp}'.");
|
||||
|
||||
// Vendor-specific formats validation
|
||||
if (format.HasFlag(OtpUriFormat.Apple))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.Issuer) || Uri.CheckHostName(config.Issuer) != UriHostNameType.Dns)
|
||||
errors.Add($"- '{nameof(config.Issuer)}' is required and must be a valid DNS name.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.IssuerLabel))
|
||||
errors.Add($"- '{nameof(config.IssuerLabel)}' is required and must be a display name for the issuer.");
|
||||
|
||||
if (((byte[])config.Secret).Length < 20)
|
||||
errors.Add($"- '{nameof(config.Secret)}' is required and must be at least 20 bytes long.");
|
||||
}
|
||||
|
||||
// All vendors, except Apple reccommend that
|
||||
// if issuer label is specified, it should be the same as the issuer
|
||||
if (!format.HasFlag(OtpUriFormat.Apple) &&
|
||||
!string.IsNullOrWhiteSpace(config.IssuerLabel) && config.IssuerLabel != config.Issuer)
|
||||
errors.Add($"- (optional) '{nameof(config.IssuerLabel)}' should be the same as '{nameof(config.Issuer)}'.");
|
||||
|
||||
if (format.HasFlag(OtpUriFormat.Yubico))
|
||||
{
|
||||
if (config.Type == OtpType.Totp && config.Period is not 15 or 30 or 60)
|
||||
errors.Add($"- '{nameof(config.Period)}' must be 15, 30 or 60.");
|
||||
}
|
||||
|
||||
// Check for digits value
|
||||
if (config.Digits is not 6 or 8)
|
||||
{
|
||||
// Now it's time for IBM and Yubico to be weird
|
||||
if (format.HasFlag(OtpUriFormat.IBM) && config.Digits is not 7 and not 9)
|
||||
errors.Add($"- '{nameof(config.Digits)}' must be 6-9.");
|
||||
|
||||
if (format.HasFlag(OtpUriFormat.Yubico) && config.Digits is not 7)
|
||||
errors.Add($"- '{nameof(config.Digits)}' must be 6-8.");
|
||||
|
||||
else
|
||||
errors.Add($"- '{nameof(config.Digits)}' must be 6 or 8.");
|
||||
}
|
||||
|
||||
// Algorithm validation
|
||||
if (!config.Algorithm.IsStandard())
|
||||
{
|
||||
// IIJ can also have an MD5 algorithm
|
||||
if (format.HasFlag(OtpUriFormat.IIJ) && config.Algorithm != OtpAlgorithm.MD5)
|
||||
errors.Add($"- '{nameof(config.Algorithm)}' must be a standard algorithm, defined by IIJ (SHA1/256/512 or MD5).");
|
||||
else
|
||||
errors.Add($"- '{nameof(config.Algorithm)}' must be a standard algorithm (SHA1, SHA256 or SHA512).");
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
error = string.Join("\n", errors);
|
||||
return false;
|
||||
}
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Xml.Serialization;
|
||||
using SimpleOTP.Encoding;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
// THIS IS THE BASE OF A PARTIAL CLASS
|
||||
// List of files
|
||||
// - OtpSecret.Base.cs - Base file
|
||||
// - OtpSecret.Static.cs - Static members
|
||||
// - OtpSecret.Serialization.cs - JSON/XML serialization members and attributes
|
||||
|
||||
/// <summary>
|
||||
/// Represents a one-time password secret.
|
||||
/// </summary>
|
||||
public partial class OtpSecret : IEquatable<OtpSecret>, IEquatable<byte[]>, IXmlSerializable, IDisposable
|
||||
{
|
||||
private readonly byte[] _secret;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpSecret"/> class with a default length of 20 bytes (160 bits).
|
||||
/// </summary>
|
||||
public OtpSecret() : this(20) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpSecret"/> class with a random secret of the specified length.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 20 bytes (160 bits) is the recommended key length specified by <a href="https://datatracker.ietf.org/doc/html/rfc4226#section-4">RFC 4226</a>.
|
||||
/// Minimal recommended length is 16 bytes (128 bits).
|
||||
/// </remarks>
|
||||
/// <param name="length">The length of the secret in bytes.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="length"/> is less than 1.</exception>
|
||||
public OtpSecret(int length) =>
|
||||
_secret = RandomNumberGenerator.GetBytes(length);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpSecret"/> class from a byte array.
|
||||
/// </summary>
|
||||
/// <param name="secret">The byte array.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="secret"/> is <c>null</c> or empty.</exception>
|
||||
public OtpSecret(byte[] secret)
|
||||
{
|
||||
if (secret is null || secret.Length < 1)
|
||||
throw new ArgumentNullException(nameof(secret));
|
||||
|
||||
_secret = secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpSecret"/> class from a Base32-encoded string (RFC 4648 §6).
|
||||
/// </summary>
|
||||
/// <param name="secret">The Base32-encoded string.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="secret"/> is <c>null</c>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="secret"/> is empty or contains invalid characters or only whitespace.</exception>
|
||||
public OtpSecret(string secret) : this(secret, DefaultEncoder) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OtpSecret"/> class from an encoded string.
|
||||
/// </summary>
|
||||
/// <param name="secret">The encoded string.</param>
|
||||
/// <param name="encoder">The encoder.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="secret"/> is <c>null</c> or empty.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="secret"/> is empty or contains invalid characters or only whitespace.</exception>
|
||||
public OtpSecret(string secret, IEncoder encoder)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(secret, nameof(secret));
|
||||
_secret = encoder.GetBytes(secret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Base32-encoded string representation of the current <see cref="OtpSecret"/> object.
|
||||
/// </summary>
|
||||
/// <returns>The Base32-encoded string representation of the current <see cref="OtpSecret"/> object.</returns>
|
||||
public override string ToString() =>
|
||||
DefaultEncoder.EncodeBytes(_secret);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the current <see cref="OtpSecret"/> object.
|
||||
/// </summary>
|
||||
/// <param name="encoder">The encoder.</param>
|
||||
/// <returns>The string representation of the current <see cref="OtpSecret"/> object.</returns>
|
||||
public string ToString(IEncoder encoder) =>
|
||||
encoder.EncodeBytes(_secret);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(OtpSecret? other) =>
|
||||
other is not null && _secret.SequenceEqual(other._secret);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is OtpSecret other && Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(byte[]? other) =>
|
||||
other is not null && _secret.SequenceEqual(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() =>
|
||||
new BigInteger(_secret ?? []).GetHashCode();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
Array.Clear(_secret, 0, _secret.Length);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Xml;
|
||||
using System.Xml.Schema;
|
||||
using SimpleOTP.Converters;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
// THIS IS THE SECTION OF A PARTIAL CLASS
|
||||
// Description: Section of OtpSecret class that holds JSON/XML serialization members and attributes
|
||||
// Base file: OtpSecret.Base.cs
|
||||
|
||||
[Serializable]
|
||||
[JsonConverter(typeof(OtpSecretJsonConverter))]
|
||||
public partial class OtpSecret
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public XmlSchema? GetSchema() => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ReadXml(XmlReader reader)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
|
||||
if (reader.NodeType != XmlNodeType.Element)
|
||||
throw new XmlException("Invalid XML element.");
|
||||
|
||||
byte[] secret = DefaultEncoder.GetBytes(reader.ReadElementContentAsString());
|
||||
|
||||
#pragma warning disable CS9193 // Argument should be a variable because it is passed to a 'ref readonly' parameter
|
||||
Unsafe.AsRef(this) = new OtpSecret(secret);
|
||||
#pragma warning restore CS9193 // Argument should be a variable because it is passed to a 'ref readonly' parameter
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void WriteXml(XmlWriter writer)
|
||||
{
|
||||
writer.WriteAttributeString("encoding", DefaultEncoder.Scheme);
|
||||
writer.WriteAttributeString("length", _secret.Length.ToString());
|
||||
writer.WriteString(DefaultEncoder.EncodeBytes(_secret));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using SimpleOTP.Encoding;
|
||||
|
||||
namespace SimpleOTP;
|
||||
|
||||
// THIS IS THE SECTION OF A PARTIAL CLASS
|
||||
// Description: Section of OtpSecret class that holds static members
|
||||
// Base file: OtpSecret.Base.cs
|
||||
|
||||
public partial class OtpSecret
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the default encoder for parsing/encoding/serializing secrets.
|
||||
/// </summary>
|
||||
public static IEncoder DefaultEncoder { get; set; } = Base32Encoder.Instance;
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static implicit operator byte[](OtpSecret secret) => secret._secret;
|
||||
public static implicit operator string(OtpSecret secret) => secret.ToString();
|
||||
|
||||
public static explicit operator OtpSecret(byte[] secret) => new(secret);
|
||||
public static explicit operator OtpSecret(string secret) => new(secret);
|
||||
|
||||
public static bool operator ==(OtpSecret left, OtpSecret right) => left.Equals(right);
|
||||
public static bool operator ==(OtpSecret left, byte[] right) => left.Equals(right);
|
||||
public static bool operator ==(OtpSecret left, string right) => left.Equals(right);
|
||||
public static bool operator ==(byte[] left, OtpSecret right) => right.Equals(left);
|
||||
public static bool operator ==(string left, OtpSecret right) => right.Equals(left);
|
||||
|
||||
public static bool operator !=(OtpSecret left, OtpSecret right) => !(left == right);
|
||||
public static bool operator !=(OtpSecret left, byte[] right) => !(left == right);
|
||||
public static bool operator !=(OtpSecret left, string right) => !(left == right);
|
||||
public static bool operator !=(byte[] left, OtpSecret right) => !(left == right);
|
||||
public static bool operator !=(string left, OtpSecret right) => !(left == right);
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the specified <see cref="OtpSecret"/> object.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="OtpSecret"/> object to copy.</param>
|
||||
/// <returns>A copy of the specified <see cref="OtpSecret"/> object.</returns>
|
||||
public static OtpSecret CreateCopy(OtpSecret source)
|
||||
{
|
||||
byte[] bytes = new byte[source._secret.Length];
|
||||
Array.Copy(source._secret, bytes, source._secret.Length);
|
||||
return new(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new random <see cref="OtpSecret"/> object with a default length of 20 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 20 bytes (160 bits) is the recommended key length specified by <a href="https://datatracker.ietf.org/doc/html/rfc4226#section-4">RFC 4226</a>.
|
||||
/// Minimal recommended length is 16 bytes (128 bits).
|
||||
/// </remarks>
|
||||
/// <returns>A new random <see cref="OtpSecret"/> object.</returns>
|
||||
public static OtpSecret CreateNew() =>
|
||||
new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new random <see cref="OtpSecret"/> object with the specified length.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 20 bytes (160 bits) is the recommended key length specified by <a href="https://datatracker.ietf.org/doc/html/rfc4226#section-4">RFC 4226</a>.
|
||||
/// Minimal recommended length is 16 bytes (128 bits).
|
||||
/// </remarks>
|
||||
/// <param name="length">The length of the secret in bytes.</param>
|
||||
/// <returns>A new random <see cref="OtpSecret"/> object.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="length"/> is less than 1.</exception>
|
||||
public static OtpSecret CreateNew(int length) =>
|
||||
new(length);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Base32-encoded string into an <see cref="OtpSecret"/> object.
|
||||
/// </summary>
|
||||
/// <param name="secret">The Base32-encoded string.</param>
|
||||
/// <returns>An <see cref="OtpSecret"/> object.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="secret"/> is <c>null</c>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="secret"/> is empty or contains invalid characters or only whitespace.</exception>
|
||||
public static OtpSecret Parse(string secret) =>
|
||||
new(secret);
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Base32-encoded string into an <see cref="OtpSecret"/> object.
|
||||
/// </summary>
|
||||
/// <param name="secret">The Base32-encoded string.</param>
|
||||
/// <param name="encoder">The encoder.</param>
|
||||
/// <returns>An <see cref="OtpSecret"/> object.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="secret"/> is <c>null</c>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="secret"/> is empty or contains invalid characters or only whitespace.</exception>
|
||||
public static OtpSecret Parse(string secret, IEncoder encoder) =>
|
||||
new(secret, encoder);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a Base32-encoded string into an <see cref="OtpSecret"/> object.
|
||||
/// </summary>
|
||||
/// <param name="secret">The Base32-encoded string.</param>
|
||||
/// <param name="otpSecret">When this method returns, contains the <see cref="OtpSecret"/> object, if the conversion succeeded, or <c>default</c> if the conversion failed.</param>
|
||||
/// <returns><c>true</c> if <paramref name="secret"/> was converted successfully; otherwise, <c>false</c>.</returns>
|
||||
public static bool TryParse(string secret, out OtpSecret? otpSecret) =>
|
||||
TryParse(secret, DefaultEncoder, out otpSecret);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a Base32-encoded string into an <see cref="OtpSecret"/> object.
|
||||
/// </summary>
|
||||
/// <param name="secret">The Base32-encoded string.</param>
|
||||
/// <param name="encoder">The encoder.</param>
|
||||
/// <param name="otpSecret">When this method returns, contains the <see cref="OtpSecret"/> object, if the conversion succeeded, or <c>default</c> if the conversion failed.</param>
|
||||
/// <returns><c>true</c> if <paramref name="secret"/> was converted successfully; otherwise, <c>false</c>.</returns>
|
||||
public static bool TryParse(string secret, IEncoder encoder, out OtpSecret? otpSecret)
|
||||
{
|
||||
try
|
||||
{
|
||||
otpSecret = new(secret, encoder);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
otpSecret = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="OtpSecret"/> object from a byte array.
|
||||
/// </summary>
|
||||
/// <param name="secret">The byte array.</param>
|
||||
/// <returns>An <see cref="OtpSecret"/> object.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="secret"/> is <c>null</c>.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><paramref name="secret"/> is empty.</exception>
|
||||
public static OtpSecret FromBytes(byte[] secret) =>
|
||||
new(secret);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace SimpleOTP;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the type of One-Time Password (OTP).
|
||||
/// </summary>
|
||||
public enum OtpType
|
||||
{
|
||||
/// <summary>
|
||||
/// Time-based One-Time Password (TOTP).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <a href="https://tools.ietf.org/html/rfc6238">RFC 6238</a>
|
||||
/// </remarks>
|
||||
Totp = 0,
|
||||
/// <summary>
|
||||
/// HMAC-based One-Time Password (HOTP).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <a href="https://tools.ietf.org/html/rfc4226">RFC 4226</a>
|
||||
/// </remarks>
|
||||
Hotp = 1
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace SimpleOTP;
|
||||
|
||||
/// <summary>
|
||||
/// Bitwise flags for specifying the format of One-Time Password (OTP) URIs.
|
||||
/// </summary>
|
||||
public enum OtpUriFormat : ushort
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a minimal URI format - only non-default properties are included.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the default format.
|
||||
/// </remarks>
|
||||
Minimal = 0b_0000_0001,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a full URI format - all properties are included.
|
||||
/// </summary>
|
||||
Full = 0b_0000_0010,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Google URI format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the default format.<br />
|
||||
/// <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Google Authenticator. Key Uri Format</a>
|
||||
/// </remarks>
|
||||
Google = 0b_0001_0000,
|
||||
|
||||
/// <summary>
|
||||
/// Represents an Apple URI format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <a href="https://developer.apple.com/documentation/authenticationservices/securing_logins_with_icloud_keychain_verification_codes">
|
||||
/// Apple. Securing Logins with iCloud Keychain Verification Codes
|
||||
/// </a>
|
||||
/// </remarks>
|
||||
Apple = 0b_0010_0000,
|
||||
|
||||
/// <summary>
|
||||
/// Represents an IBM URI format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <a href="https://www.ibm.com/docs/en/sva/9.0.6?topic=authentication-configuring-totp-one-time-password-mechanism">
|
||||
/// IBM. Authentication Configuring TOTP One-Time Password Mechanism
|
||||
/// </a>
|
||||
/// </remarks>
|
||||
IBM = 0b_0100_0000,
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Yubico URI format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <a href="https://docs.yubico.com/yesdk/users-manual/application-oath/uri-string-format.html">
|
||||
/// Yubico. URI String Format
|
||||
/// </a>
|
||||
/// </remarks>
|
||||
Yubico = 0b_1000_0000,
|
||||
|
||||
/// <summary>
|
||||
/// Represents an IIJ URI format.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <a href="https://www1.auth.iij.jp/smartkey/en/uri_v1.html">Internet Initiative Japan. URI format</a>
|
||||
/// </remarks>
|
||||
IIJ = 0b_0001_0000_0000
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>EugeneFox.SimpleOTP</PackageId>
|
||||
<Version>8.0.0.0-rc1</Version>
|
||||
<Authors>Eugene Fox</Authors>
|
||||
<Copyright>Copyright © Eugene Fox 2024</Copyright>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/XFox111/SimpleOTP.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/XFox111/SimpleOTP</PackageProjectUrl>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageTags>otp;totp;hotp;authenticator;authentication;one-time;2fa;mfa;security;otpauth</PackageTags>
|
||||
<Description>
|
||||
Feature-rich, fast, and customizable library for implementation TOTP/HOTP authenticators and validators.
|
||||
</Description>
|
||||
<PackageReleaseNotes>
|
||||
(BREAKING CHANGE) Complete overhaul of the library. See https://github.com/XFox111/SimpleOTP/releases/tag/2.0.0 for more details.
|
||||
</PackageReleaseNotes>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\assets\icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
<None Include="..\..\README.md">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace SimpleOTP;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a span of tolerance values used in OTP (One-Time Password) validation.
|
||||
/// </summary>
|
||||
/// <param name="behind">The number of periods/counter values behind the current value.</param>
|
||||
/// <param name="ahead">The number of periods/counter values ahead of the current value.</param>
|
||||
public readonly struct ToleranceSpan(int behind, int ahead) : IEquatable<ToleranceSpan>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default recommended <see cref="ToleranceSpan"/> value.
|
||||
/// </summary>
|
||||
/// <value>The default <see cref="ToleranceSpan"/> value: 1 counter/period ahead and behind.</value>
|
||||
public static ToleranceSpan Default { get; } = new(1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of tolerance values behind the current value.
|
||||
/// </summary>
|
||||
public int Behind { get; init; } = behind;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of tolerance values ahead of the current value.
|
||||
/// </summary>
|
||||
public int Ahead { get; init; } = ahead;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ToleranceSpan"/> struct with the specified tolerance value.
|
||||
/// The <see cref="Behind"/> and <see cref="Ahead"/> properties will be set to the same value.
|
||||
/// </summary>
|
||||
/// <param name="tolerance">The tolerance value to set for both <see cref="Behind"/> and <see cref="Ahead"/>.</param>
|
||||
public ToleranceSpan(int tolerance) : this(tolerance, tolerance) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(ToleranceSpan other) =>
|
||||
Behind == other.Behind && Ahead == other.Ahead;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is ToleranceSpan span && Equals(span);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(Behind, Ahead);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the string representation of the <see cref="ToleranceSpan"/> struct.
|
||||
/// </summary>
|
||||
/// <returns>The string representation of the <see cref="ToleranceSpan"/> struct.</returns>
|
||||
public override string ToString() =>
|
||||
$"(-{Behind}, +{Ahead})";
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
public static implicit operator ToleranceSpan(int tolerance) => new(tolerance);
|
||||
public static implicit operator ToleranceSpan((int behind, int ahead) tolerance) => new(tolerance.behind, tolerance.ahead);
|
||||
|
||||
public static bool operator ==(ToleranceSpan left, ToleranceSpan right) => left.Equals(right);
|
||||
public static bool operator !=(ToleranceSpan left, ToleranceSpan right) => !(left == right);
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
namespace SimpleOTP;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Time-based One-Time Password (TOTP) generator.
|
||||
/// </summary>
|
||||
public class Totp : Otp
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the time period (in seconds) for which each generated OTP is valid.
|
||||
/// </summary>
|
||||
/// <remarks>Also used to calculate the current counter value.</remarks>
|
||||
public int Period { get; set; } = 30;
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Totp"/> class with the specified secret key.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
public Totp(OtpSecret secret) : base(secret) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Totp"/> class with the specified secret key and number of OTP code digits.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="period">The time period (in seconds) for which each generated OTP is valid.</param>
|
||||
public Totp(OtpSecret secret, int period) : base(secret) =>
|
||||
Period = period;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Totp"/> class with the specified secret key, number of OTP code digits, and time period.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="period">The time period (in seconds) for which each generated OTP is valid.</param>
|
||||
/// <param name="digits">The number of digits in the OTP code.</param>
|
||||
public Totp(OtpSecret secret, int period, int digits) : base(secret, digits) =>
|
||||
Period = period;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Totp"/> class with the specified secret key, hash algorithm, and time period.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="period">The time period (in seconds) for which each generated OTP is valid.</param>
|
||||
/// <param name="algorithm">The algorithm used for generating OTP codes.</param>
|
||||
public Totp(OtpSecret secret, int period, OtpAlgorithm algorithm) : base(secret, algorithm) =>
|
||||
Period = period;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Totp"/> class with the specified secret key, hash algorithm, number of digits, and time period.
|
||||
/// </summary>
|
||||
/// <param name="secret">The secret key used for generating OTP codes.</param>
|
||||
/// <param name="period">The time period (in seconds) for which each generated OTP is valid.</param>
|
||||
/// <param name="algorithm">The algorithm used for generating OTP codes.</param>
|
||||
/// <param name="digits">The number of digits in the OTP code.</param>
|
||||
public Totp(OtpSecret secret, int period, OtpAlgorithm algorithm, int digits) : base(secret, algorithm, digits) =>
|
||||
Period = period;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Generates an OTP based on the specified counter value.
|
||||
/// </summary>
|
||||
/// <param name="counter">The counter value to use for OTP generation.</param>
|
||||
/// <returns>An instance of <see cref="OtpCode"/> representing the generated OTP.</returns>
|
||||
public override OtpCode Generate(long counter) =>
|
||||
new(Compute(counter), Digits, DateTime.UnixEpoch.AddSeconds((counter + 1) * Period));
|
||||
|
||||
/// <summary>
|
||||
/// Generates an OTP based on the specified date and time.
|
||||
/// </summary>
|
||||
/// <param name="date">The date and time to use for OTP generation.</param>
|
||||
/// <returns>An instance of <see cref="OtpCode"/> representing the generated OTP.</returns>
|
||||
public OtpCode Generate(DateTimeOffset date) =>
|
||||
Generate(date.ToUnixTimeSeconds() / Period);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Validates an OTP code with tolerance and base counter value, and returns the resynchronization value.
|
||||
/// </summary>
|
||||
/// <param name="code">The OTP code to validate.</param>
|
||||
/// <param name="tolerance">The tolerance span for code validation.</param>
|
||||
/// <param name="baseTime">The base timestamp value.</param>
|
||||
/// <param name="resyncValue">The resynchronization value. Indicates how much given OTP code is ahead or behind the current counter value.</param>
|
||||
/// <returns><c>true</c> if the OTP code is valid; otherwise, <c>false</c>.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Implementation for the <see cref="Otp.Algorithm"/> algorithm was not found.
|
||||
/// Use <see cref="HashAlgorithmProviders.AddProvider(OtpAlgorithm)"/> to register an implementation.
|
||||
/// </exception>
|
||||
public bool Validate(OtpCode code, ToleranceSpan tolerance, DateTimeOffset baseTime, out int resyncValue) =>
|
||||
Validate(code, tolerance, baseTime.ToUnixTimeSeconds() / 30, out resyncValue);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current counter value based on the current UTC time and the configured time period.
|
||||
/// </summary>
|
||||
/// <returns>The current counter value.</returns>
|
||||
protected override long GetCounter() =>
|
||||
DateTimeOffset.UtcNow.ToUnixTimeSeconds() / Period;
|
||||
}
|
||||
Reference in New Issue
Block a user