aboutsummaryrefslogtreecommitdiff
path: root/VNLib.Plugins.Essentials.Accounts/MFA
diff options
context:
space:
mode:
Diffstat (limited to 'VNLib.Plugins.Essentials.Accounts/MFA')
-rw-r--r--VNLib.Plugins.Essentials.Accounts/MFA/FidoAuthenticatorSelection.cs40
-rw-r--r--VNLib.Plugins.Essentials.Accounts/MFA/FidoRegClientData.cs40
-rw-r--r--VNLib.Plugins.Essentials.Accounts/MFA/FidoRegistrationMessage.cs52
-rw-r--r--VNLib.Plugins.Essentials.Accounts/MFA/MFAConfig.cs103
-rw-r--r--VNLib.Plugins.Essentials.Accounts/MFA/MFAType.cs31
-rw-r--r--VNLib.Plugins.Essentials.Accounts/MFA/MFAUpgrade.cs65
-rw-r--r--VNLib.Plugins.Essentials.Accounts/MFA/UserMFAExtensions.cs384
7 files changed, 0 insertions, 715 deletions
diff --git a/VNLib.Plugins.Essentials.Accounts/MFA/FidoAuthenticatorSelection.cs b/VNLib.Plugins.Essentials.Accounts/MFA/FidoAuthenticatorSelection.cs
deleted file mode 100644
index 0ea6dad..0000000
--- a/VNLib.Plugins.Essentials.Accounts/MFA/FidoAuthenticatorSelection.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.Accounts
-* File: FidoAuthenticatorSelection.cs
-*
-* FidoAuthenticatorSelection.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
-* VNLib collection of libraries and utilities.
-*
-* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify
-* it under the terms of the GNU Affero General Public License as
-* published by the Free Software Foundation, either version 3 of the
-* License, or (at your option) any later version.
-*
-* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU Affero General Public License for more details.
-*
-* You should have received a copy of the GNU Affero General Public License
-* along with this program. If not, see https://www.gnu.org/licenses/.
-*/
-
-using System.Text.Json.Serialization;
-
-#nullable enable
-
-namespace VNLib.Plugins.Essentials.Accounts.MFA
-{
- class FidoAuthenticatorSelection
- {
- [JsonPropertyName("requireResidentKey")]
- public bool RequireResidentKey { get; set; } = false;
- [JsonPropertyName("authenticatorAttachment")]
- public string? AuthenticatorAttachment { get; set; } = "cross-platform";
- [JsonPropertyName("userVerification")]
- public string? UserVerification { get; set; } = "required";
- }
-}
diff --git a/VNLib.Plugins.Essentials.Accounts/MFA/FidoRegClientData.cs b/VNLib.Plugins.Essentials.Accounts/MFA/FidoRegClientData.cs
deleted file mode 100644
index 1ef7d59..0000000
--- a/VNLib.Plugins.Essentials.Accounts/MFA/FidoRegClientData.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.Accounts
-* File: FidoRegClientData.cs
-*
-* FidoRegClientData.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
-* VNLib collection of libraries and utilities.
-*
-* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify
-* it under the terms of the GNU Affero General Public License as
-* published by the Free Software Foundation, either version 3 of the
-* License, or (at your option) any later version.
-*
-* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU Affero General Public License for more details.
-*
-* You should have received a copy of the GNU Affero General Public License
-* along with this program. If not, see https://www.gnu.org/licenses/.
-*/
-
-using System.Text.Json.Serialization;
-
-#nullable enable
-
-namespace VNLib.Plugins.Essentials.Accounts.MFA
-{
- internal class FidoRegClientData
- {
- [JsonPropertyName("challenge")]
- public string? Challenge { get; set; }
- [JsonPropertyName("origin")]
- public string? Origin { get; set; }
- [JsonPropertyName("type")]
- public string? Type { get; set; }
- }
-}
diff --git a/VNLib.Plugins.Essentials.Accounts/MFA/FidoRegistrationMessage.cs b/VNLib.Plugins.Essentials.Accounts/MFA/FidoRegistrationMessage.cs
deleted file mode 100644
index e8fbcc4..0000000
--- a/VNLib.Plugins.Essentials.Accounts/MFA/FidoRegistrationMessage.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.Accounts
-* File: FidoRegistrationMessage.cs
-*
-* FidoRegistrationMessage.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
-* VNLib collection of libraries and utilities.
-*
-* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify
-* it under the terms of the GNU Affero General Public License as
-* published by the Free Software Foundation, either version 3 of the
-* License, or (at your option) any later version.
-*
-* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU Affero General Public License for more details.
-*
-* You should have received a copy of the GNU Affero General Public License
-* along with this program. If not, see https://www.gnu.org/licenses/.
-*/
-
-using System.Text.Json.Serialization;
-
-#nullable enable
-
-namespace VNLib.Plugins.Essentials.Accounts.MFA
-{
- /// <summary>
- /// Represents a fido device registration message to be sent
- /// to a currently signed in user
- /// </summary>
- class FidoRegistrationMessage
- {
- [JsonPropertyName("id")]
- public string? GuidUserId { get; set; }
- [JsonPropertyName("challenge")]
- public string? Base64Challenge { get; set; } = null;
- [JsonPropertyName("timeout")]
- public int Timeout { get; set; } = 60000;
- [JsonPropertyName("cose_alg")]
- public int CoseAlgNumber { get; set; }
- [JsonPropertyName("rp_name")]
- public string? SiteName { get; set; }
- [JsonPropertyName("attestation")]
- public string? AttestationType { get; set; } = "none";
- [JsonPropertyName("authenticatorSelection")]
- public FidoAuthenticatorSelection? AuthSelection { get; set; } = new();
- }
-}
diff --git a/VNLib.Plugins.Essentials.Accounts/MFA/MFAConfig.cs b/VNLib.Plugins.Essentials.Accounts/MFA/MFAConfig.cs
deleted file mode 100644
index 03d5a20..0000000
--- a/VNLib.Plugins.Essentials.Accounts/MFA/MFAConfig.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.Accounts
-* File: MFAConfig.cs
-*
-* MFAConfig.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
-* VNLib collection of libraries and utilities.
-*
-* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify
-* it under the terms of the GNU Affero General Public License as
-* published by the Free Software Foundation, either version 3 of the
-* License, or (at your option) any later version.
-*
-* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU Affero General Public License for more details.
-*
-* You should have received a copy of the GNU Affero General Public License
-* along with this program. If not, see https://www.gnu.org/licenses/.
-*/
-
-using System;
-using System.Linq;
-using System.Text.Json;
-using System.Collections.Generic;
-
-using VNLib.Hashing;
-using VNLib.Utils.Extensions;
-using VNLib.Hashing.IdentityUtility;
-
-namespace VNLib.Plugins.Essentials.Accounts.MFA
-{
- internal class MFAConfig
- {
- public ReadOnlyJsonWebKey? MFASecret { get; set; }
-
- public bool TOTPEnabled { get; }
- public string? IssuerName { get; }
- public TimeSpan TOTPPeriod { get; }
- public HashAlg TOTPAlg { get; }
- public int TOTPDigits { get; }
- public int TOTPSecretBytes { get; }
- public int TOTPTimeWindowSteps { get; }
-
-
- public bool FIDOEnabled { get; }
- public int FIDOChallangeSize { get; }
- public int FIDOTimeout { get; }
- public string? FIDOSiteName { get; }
- public string? FIDOAttestationType { get; }
- public FidoAuthenticatorSelection? FIDOAuthSelection { get; }
-
- public TimeSpan UpgradeValidFor { get; }
- public int NonceLenBytes { get; }
-
- public MFAConfig(IReadOnlyDictionary<string, JsonElement> conf)
- {
- UpgradeValidFor = conf["upgrade_expires_secs"].GetTimeSpan(TimeParseType.Seconds);
- NonceLenBytes = conf["nonce_size"].GetInt32();
- string siteName = conf["site_name"].GetString() ?? throw new KeyNotFoundException("Missing required key 'site_name' in 'mfa' config");
-
- //Totp setup
- if (conf.TryGetValue("totp", out JsonElement totpEl))
- {
- IReadOnlyDictionary<string, JsonElement> totp = totpEl.EnumerateObject().ToDictionary(k => k.Name, k => k.Value);
-
- //Get totp config
- IssuerName = siteName;
- //Get alg name
- string TOTPAlgName = totp["algorithm"].GetString()?.ToUpper() ?? throw new KeyNotFoundException("Missing required key 'algorithm' in plugin 'mfa' config");
- //Parse from enum string
- TOTPAlg = Enum.Parse<HashAlg>(TOTPAlgName);
-
-
- TOTPDigits = totp["digits"].GetInt32();
- TOTPPeriod = TimeSpan.FromSeconds(totp["period_secs"].GetInt32());
- TOTPSecretBytes = totp["secret_size"].GetInt32();
- TOTPTimeWindowSteps = totp["window_size"].GetInt32();
- //Set enabled flag
- TOTPEnabled = true;
- }
- //Fido setup
- if(conf.TryGetValue("fido", out JsonElement fidoEl))
- {
- IReadOnlyDictionary<string, JsonElement> fido = fidoEl.EnumerateObject().ToDictionary(k => k.Name, k => k.Value);
- FIDOChallangeSize = fido["challenge_size"].GetInt32();
- FIDOAttestationType = fido["attestation"].GetString();
- FIDOTimeout = fido["timeout"].GetInt32();
- FIDOSiteName = siteName;
- //Deserailze a
- if(fido.TryGetValue("authenticatorSelection", out JsonElement authSel))
- {
- FIDOAuthSelection = authSel.Deserialize<FidoAuthenticatorSelection>();
- }
- //Set enabled flag
- FIDOEnabled = true;
- }
- }
- }
-}
diff --git a/VNLib.Plugins.Essentials.Accounts/MFA/MFAType.cs b/VNLib.Plugins.Essentials.Accounts/MFA/MFAType.cs
deleted file mode 100644
index 208eea3..0000000
--- a/VNLib.Plugins.Essentials.Accounts/MFA/MFAType.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.Accounts
-* File: MFAType.cs
-*
-* MFAType.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
-* VNLib collection of libraries and utilities.
-*
-* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify
-* it under the terms of the GNU Affero General Public License as
-* published by the Free Software Foundation, either version 3 of the
-* License, or (at your option) any later version.
-*
-* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU Affero General Public License for more details.
-*
-* You should have received a copy of the GNU Affero General Public License
-* along with this program. If not, see https://www.gnu.org/licenses/.
-*/
-
-namespace VNLib.Plugins.Essentials.Accounts.MFA
-{
- public enum MFAType
- {
- TOTP, FIDO, PGP
- }
-}
diff --git a/VNLib.Plugins.Essentials.Accounts/MFA/MFAUpgrade.cs b/VNLib.Plugins.Essentials.Accounts/MFA/MFAUpgrade.cs
deleted file mode 100644
index 5577d51..0000000
--- a/VNLib.Plugins.Essentials.Accounts/MFA/MFAUpgrade.cs
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.Accounts
-* File: MFAUpgrade.cs
-*
-* MFAUpgrade.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
-* VNLib collection of libraries and utilities.
-*
-* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify
-* it under the terms of the GNU Affero General Public License as
-* published by the Free Software Foundation, either version 3 of the
-* License, or (at your option) any later version.
-*
-* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU Affero General Public License for more details.
-*
-* You should have received a copy of the GNU Affero General Public License
-* along with this program. If not, see https://www.gnu.org/licenses/.
-*/
-
-using System.Text.Json.Serialization;
-
-#nullable enable
-
-namespace VNLib.Plugins.Essentials.Accounts.MFA
-{
- internal class MFAUpgrade
- {
- /// <summary>
- /// The login's client id specifier
- /// </summary>
- [JsonPropertyName("cid")]
- public string? ClientID { get; set; }
- /// <summary>
- /// The id of the user that is requesting a login
- /// </summary>
- [JsonPropertyName("uname")]
- public string? UserName{ get; set; }
- /// <summary>
- /// The <see cref="MFAType"/> of the upgrade request
- /// </summary>
- [JsonPropertyName("type")]
- public MFAType Type { get; set; }
- /// <summary>
- /// The a base64 encoded string of the user's
- /// public key
- /// </summary>
- [JsonPropertyName("pubkey")]
- public string? Base64PubKey { get; set; }
- /// <summary>
- /// The user's specified language
- /// </summary>
- [JsonPropertyName("lang")]
- public string? ClientLocalLanguage { get; set; }
- /// <summary>
- /// The encrypted password token for the client
- /// </summary>
- [JsonPropertyName("cd")]
- public string? PwClientData { get; set; }
- }
-}
diff --git a/VNLib.Plugins.Essentials.Accounts/MFA/UserMFAExtensions.cs b/VNLib.Plugins.Essentials.Accounts/MFA/UserMFAExtensions.cs
deleted file mode 100644
index 1ec9953..0000000
--- a/VNLib.Plugins.Essentials.Accounts/MFA/UserMFAExtensions.cs
+++ /dev/null
@@ -1,384 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.Accounts
-* File: UserMFAExtensions.cs
-*
-* UserMFAExtensions.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
-* VNLib collection of libraries and utilities.
-*
-* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify
-* it under the terms of the GNU Affero General Public License as
-* published by the Free Software Foundation, either version 3 of the
-* License, or (at your option) any later version.
-*
-* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU Affero General Public License for more details.
-*
-* You should have received a copy of the GNU Affero General Public License
-* along with this program. If not, see https://www.gnu.org/licenses/.
-*/
-
-using System;
-using System.Linq;
-using System.Text.Json;
-using System.Collections.Generic;
-using System.Security.Cryptography;
-using System.Text.Json.Serialization;
-using System.Diagnostics.CodeAnalysis;
-
-using VNLib.Hashing;
-using VNLib.Utils;
-using VNLib.Utils.Memory;
-using VNLib.Utils.Extensions;
-using VNLib.Hashing.IdentityUtility;
-using VNLib.Plugins.Essentials.Users;
-using VNLib.Plugins.Essentials.Sessions;
-using VNLib.Plugins.Extensions.Loading;
-
-namespace VNLib.Plugins.Essentials.Accounts.MFA
-{
-
- internal static class UserMFAExtensions
- {
- public const string WEBAUTHN_KEY_ENTRY = "mfa.fido";
- public const string TOTP_KEY_ENTRY = "mfa.totp";
- public const string PGP_PUB_KEY = "mfa.pgpp";
- public const string SESSION_SIG_KEY = "mfa.sig";
-
- /// <summary>
- /// Determines if the user account has an
- /// </summary>
- /// <param name="user"></param>
- /// <returns>True if any form of MFA is enabled for the user account</returns>
- public static bool MFAEnabled(this IUser user)
- {
- return !(string.IsNullOrWhiteSpace(user[TOTP_KEY_ENTRY]) && string.IsNullOrWhiteSpace(user[WEBAUTHN_KEY_ENTRY]));
- }
-
- #region totp
-
- /// <summary>
- /// Recovers the base32 encoded TOTP secret for the current user
- /// </summary>
- /// <param name="user"></param>
- /// <returns>The base32 encoded TOTP secret, or an emtpy string (user spec) if not set</returns>
- public static string MFAGetTOTPSecret(this IUser user) => user[TOTP_KEY_ENTRY];
-
- /// <summary>
- /// Stores or removes the current user's TOTP secret, stored in base32 format
- /// </summary>
- /// <param name="user"></param>
- /// <param name="secret">The base32 encoded TOTP secret</param>
- public static void MFASetTOTPSecret(this IUser user, string? secret) => user[TOTP_KEY_ENTRY] = secret!;
-
-
- /// <summary>
- /// Generates/overwrites the current user's TOTP secret entry and returns a
- /// byte array of the generated secret bytes
- /// </summary>
- /// <param name="entry">The <see cref="MFAEntry"/> to modify the TOTP configuration of</param>
- /// <returns>The raw secret that was encrypted and stored in the <see cref="MFAEntry"/>, to send to the client</returns>
- /// <exception cref="OutOfMemoryException"></exception>
- public static byte[] MFAGenreateTOTPSecret(this IUser user, MFAConfig config)
- {
- //Generate a random key
- byte[] newSecret = RandomHash.GetRandomBytes(config.TOTPSecretBytes);
- //Store secret in user storage
- user.MFASetTOTPSecret(VnEncoding.ToBase32String(newSecret, false));
- //return the raw secret bytes
- return newSecret;
- }
-
- /// <summary>
- /// Verfies the supplied TOTP code against the current user's totp codes
- /// This method should not be used for verifying TOTP codes for authentication
- /// </summary>
- /// <param name="user">The user account to verify the TOTP code against</param>
- /// <param name="code">The code to verify</param>
- /// <param name="config">A readonly referrence to the MFA configuration structure</param>
- /// <returns>True if the user has TOTP configured and code matches against its TOTP secret entry, false otherwise</returns>
- /// <exception cref="FormatException"></exception>
- /// <exception cref="OutOfMemoryException"></exception>
- public static bool VerifyTOTP(this MFAConfig config, IUser user, uint code)
- {
- //Get the base32 TOTP secret for the user and make sure its actually set
- string base32Secret = user.MFAGetTOTPSecret();
- if (string.IsNullOrWhiteSpace(base32Secret))
- {
- return false;
- }
- //Alloc buffer with zero o
- using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(base32Secret.Length, true);
- ERRNO count = VnEncoding.TryFromBase32Chars(base32Secret, buffer);
- //Verify the TOTP using the decrypted secret
- return count && VerifyTOTP(code, buffer.AsSpan(0, count), config);
- }
-
- private static bool VerifyTOTP(uint totpCode, ReadOnlySpan<byte> userSecret, MFAConfig config)
- {
- //A basic attempt at a constant time TOTP verification, run the calculation a fixed number of times, regardless of the resutls
- bool codeMatches = false;
-
- //cache current time
- DateTimeOffset currentUtc = DateTimeOffset.UtcNow;
- //Start the current window with the minimum window
- int currenStep = -config.TOTPTimeWindowSteps;
- Span<byte> stepBuffer = stackalloc byte[sizeof(long)];
- Span<byte> hashBuffer = stackalloc byte[(int)config.TOTPAlg];
- //Run the loop at least once to allow a 0 step tight window
- do
- {
- //Calculate the window by multiplying the window by the current step, then add it to the current time offset to produce a new window
- DateTimeOffset window = currentUtc.Add(config.TOTPPeriod.Multiply(currenStep));
- //calculate the time step
- long timeStep = (long)Math.Floor(window.ToUnixTimeSeconds() / config.TOTPPeriod.TotalSeconds);
- //try to compute the hash
- _ = BitConverter.TryWriteBytes(stepBuffer, timeStep) ? 0 : throw new InternalBufferTooSmallException("Failed to format TOTP time step");
- //If platform is little endian, reverse the byte order
- if (BitConverter.IsLittleEndian)
- {
- stepBuffer.Reverse();
- }
- ERRNO result = ManagedHash.ComputeHmac(userSecret, stepBuffer, hashBuffer, config.TOTPAlg);
- //try to compute the hash of the time step
- if (result < 1)
- {
- throw new InternalBufferTooSmallException("Failed to compute TOTP time step hash because the buffer was too small");
- }
- //Hash bytes
- ReadOnlySpan<byte> hash = hashBuffer[..(int)result];
- //compute the TOTP code and compare it to the supplied, then store the result
- codeMatches |= (totpCode == CalcTOTPCode(hash, config));
- //next step
- currenStep++;
- } while (currenStep <= config.TOTPTimeWindowSteps);
-
- return codeMatches;
- }
-
- private static uint CalcTOTPCode(ReadOnlySpan<byte> hash, MFAConfig config)
- {
- //Calculate the offset, RFC defines, the lower 4 bits of the last byte in the hash output
- byte offset = (byte)(hash[^1] & 0x0Fu);
-
- uint TOTPCode;
- if (BitConverter.IsLittleEndian)
- {
- //Store the code components
- TOTPCode = (hash[offset] & 0x7Fu) << 24 | (hash[offset + 1] & 0xFFu) << 16 | (hash[offset + 2] & 0xFFu) << 8 | hash[offset + 3] & 0xFFu;
- }
- else
- {
- //Store the code components (In reverse order for big-endian machines)
- TOTPCode = (hash[offset + 3] & 0x7Fu) << 24 | (hash[offset + 2] & 0xFFu) << 16 | (hash[offset + 1] & 0xFFu) << 8 | hash[offset] & 0xFFu;
- }
- //calculate the modulus value
- TOTPCode %= (uint)Math.Pow(10, config.TOTPDigits);
- return TOTPCode;
- }
-
- #endregion
-
- #region loading
-
- const string MFA_CONFIG_KEY = "mfa";
-
- /// <summary>
- /// Gets the plugins ambient <see cref="PasswordHashing"/> if loaded, or loads it if required. This class will
- /// be unloaded when the plugin us unloaded.
- /// </summary>
- /// <param name="plugin"></param>
- /// <returns>The ambient <see cref="PasswordHashing"/></returns>
- /// <exception cref="OverflowException"></exception>
- /// <exception cref="KeyNotFoundException"></exception>
- /// <exception cref="ObjectDisposedException"></exception>
- public static MFAConfig? GetMfaConfig(this PluginBase plugin)
- {
- static MFAConfig? LoadMfaConfig(PluginBase pbase)
- {
- //Try to get the configuration object
- IReadOnlyDictionary<string, JsonElement>? conf = pbase.TryGetConfig(MFA_CONFIG_KEY);
-
- if (conf == null)
- {
- return null;
- }
- //Init mfa config
- MFAConfig mfa = new(conf);
-
- //Recover secret from config and dangerous 'lazy load'
- _ = pbase.DeferTask(async () =>
- {
- mfa.MFASecret = await pbase.TryGetSecretAsync("mfa_secret").ToJsonWebKey();
-
- }, 50);
-
- return mfa;
- }
-
- plugin.ThrowIfUnloaded();
- //Get/load the passwords one time only
- return LoadingExtensions.GetOrCreateSingleton(plugin, LoadMfaConfig);
- }
-
- #endregion
-
- #region pgp
-
- private class PgpMfaCred
- {
- [JsonPropertyName("p")]
- public string? SpkiPublicKey { get; set; }
-
- [JsonPropertyName("c")]
- public string? CurveFriendlyName { get; set; }
- }
-
-
- /// <summary>
- /// Gets the stored PGP public key for the user
- /// </summary>
- /// <param name="user"></param>
- /// <returns>The stored PGP signature key </returns>
- public static string MFAGetPGPPubKey(this IUser user) => user[PGP_PUB_KEY];
-
- public static void MFASetPGPPubKey(this IUser user, string? pubKey) => user[PGP_PUB_KEY] = pubKey!;
-
- public static void VerifySignedData(string data)
- {
-
- }
-
- #endregion
-
- #region webauthn
-
- #endregion
-
- /// <summary>
- /// Recovers a signed MFA upgrade JWT and verifies its authenticity, and confrims its not expired,
- /// then recovers the upgrade mssage
- /// </summary>
- /// <param name="config"></param>
- /// <param name="upgradeJwtString">The signed JWT upgrade message</param>
- /// <param name="upgrade">The recovered upgrade</param>
- /// <param name="base64sessionSig">The stored base64 encoded signature from the session that requested an upgrade</param>
- /// <returns>True if the upgrade was verified, not expired, and was recovered from the signed message, false otherwise</returns>
- public static bool RecoverUpgrade(this MFAConfig config, ReadOnlySpan<char> upgradeJwtString, ReadOnlySpan<char> base64sessionSig, [NotNullWhen(true)] out MFAUpgrade? upgrade)
- {
- //Verifies a jwt stored signature against the actual signature
- static bool VerifyStoredSig(ReadOnlySpan<char> base64string, ReadOnlySpan<byte> signature)
- {
- using UnsafeMemoryHandle<byte> buffer = Memory.UnsafeAlloc<byte>(base64string.Length, true);
- //Recover base64
- ERRNO count = VnEncoding.TryFromBase64Chars(base64string, buffer.Span);
- //Compare
- return CryptographicOperations.FixedTimeEquals(signature, buffer.Span[..(int)count]);
- }
-
- //Verify config secret
- _ = config.MFASecret ?? throw new InvalidOperationException("MFA config is missing required upgrade signing key");
-
- upgrade = null;
-
- //Parse jwt
- using JsonWebToken jwt = JsonWebToken.Parse(upgradeJwtString);
-
- if (!jwt.VerifyFromJwk(config.MFASecret))
- {
- return false;
- }
-
- if(!VerifyStoredSig(base64sessionSig, jwt.SignatureData))
- {
- return false;
- }
-
- //get request body
- using JsonDocument doc = jwt.GetPayload();
- //Recover issued at time
- DateTimeOffset iat = DateTimeOffset.FromUnixTimeMilliseconds(doc.RootElement.GetProperty("iat").GetInt64());
- //Verify its not timed out
- if (iat.Add(config.UpgradeValidFor) < DateTimeOffset.UtcNow)
- {
- //expired
- return false;
- }
-
- //Recover the upgrade message
- upgrade = doc.RootElement.GetProperty("upgrade").Deserialize<MFAUpgrade>();
- return upgrade != null;
- }
-
-
- /// <summary>
- /// Generates an upgrade for the requested user, using the highest prirotiy method
- /// </summary>
- /// <param name="login">The message from the user requesting the login</param>
- /// <returns>A signed upgrade message the client will pass back to the server after the MFA verification</returns>
- /// <exception cref="InvalidOperationException"></exception>
- public static Tuple<string, string>? MFAGetUpgradeIfEnabled(this IUser user, MFAConfig? conf, LoginMessage login, string pwClientData)
- {
- //Webauthn config
-
-
- //Search for totp secret entry
- string base32Secret = user.MFAGetTOTPSecret();
-
- //Check totp entry
- if (!string.IsNullOrWhiteSpace(base32Secret))
- {
- //Verify config secret
- _ = conf?.MFASecret ?? throw new InvalidOperationException("MFA config is missing required upgrade signing key");
-
- //setup the upgrade
- MFAUpgrade upgrade = new()
- {
- //Set totp upgrade type
- Type = MFAType.TOTP,
- //Store login message details
- UserName = login.UserName,
- ClientID = login.ClientID,
- Base64PubKey = login.ClientPublicKey,
- ClientLocalLanguage = login.LocalLanguage,
- PwClientData = pwClientData
- };
-
- //Init jwt for upgrade
- return GetUpgradeMessage(upgrade, conf.MFASecret, conf.UpgradeValidFor);
- }
- return null;
- }
-
- private static Tuple<string, string> GetUpgradeMessage(MFAUpgrade upgrade, ReadOnlyJsonWebKey secret, TimeSpan expires)
- {
- //Add some random entropy to the upgrade message, to help prevent forgery
- string entropy = RandomHash.GetRandomBase32(16);
- //Init jwt
- using JsonWebToken upgradeJwt = new();
- upgradeJwt.WriteHeader(secret.JwtHeader);
- //Write claims
- upgradeJwt.InitPayloadClaim()
- .AddClaim("iat", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())
- .AddClaim("upgrade", upgrade)
- .AddClaim("type", upgrade.Type.ToString().ToLower())
- .AddClaim("expires", expires.TotalSeconds)
- .AddClaim("a", entropy)
- .CommitClaims();
-
- //Sign with jwk
- upgradeJwt.SignFromJwk(secret);
-
- //compile and return jwt upgrade
- return new(upgradeJwt.Compile(), Convert.ToBase64String(upgradeJwt.SignatureData));
- }
-
- public static void MfaUpgradeSignature(this in SessionInfo session, string? base64Signature) => session[SESSION_SIG_KEY] = base64Signature!;
-
- public static string? MfaUpgradeSignature(this in SessionInfo session) => session[SESSION_SIG_KEY];
- }
-}