From 551066ed9a255bd47c1c5789ec1998fda64bd5aa Mon Sep 17 00:00:00 2001 From: vnugent Date: Thu, 12 Jan 2023 17:47:40 -0500 Subject: Large project reorder and consolidation --- .../MFA/UserMFAExtensions.cs | 384 --------------------- 1 file changed, 384 deletions(-) delete mode 100644 VNLib.Plugins.Essentials.Accounts/MFA/UserMFAExtensions.cs (limited to 'VNLib.Plugins.Essentials.Accounts/MFA/UserMFAExtensions.cs') 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"; - - /// - /// Determines if the user account has an - /// - /// - /// True if any form of MFA is enabled for the user account - public static bool MFAEnabled(this IUser user) - { - return !(string.IsNullOrWhiteSpace(user[TOTP_KEY_ENTRY]) && string.IsNullOrWhiteSpace(user[WEBAUTHN_KEY_ENTRY])); - } - - #region totp - - /// - /// Recovers the base32 encoded TOTP secret for the current user - /// - /// - /// The base32 encoded TOTP secret, or an emtpy string (user spec) if not set - public static string MFAGetTOTPSecret(this IUser user) => user[TOTP_KEY_ENTRY]; - - /// - /// Stores or removes the current user's TOTP secret, stored in base32 format - /// - /// - /// The base32 encoded TOTP secret - public static void MFASetTOTPSecret(this IUser user, string? secret) => user[TOTP_KEY_ENTRY] = secret!; - - - /// - /// Generates/overwrites the current user's TOTP secret entry and returns a - /// byte array of the generated secret bytes - /// - /// The to modify the TOTP configuration of - /// The raw secret that was encrypted and stored in the , to send to the client - /// - 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; - } - - /// - /// Verfies the supplied TOTP code against the current user's totp codes - /// This method should not be used for verifying TOTP codes for authentication - /// - /// The user account to verify the TOTP code against - /// The code to verify - /// A readonly referrence to the MFA configuration structure - /// True if the user has TOTP configured and code matches against its TOTP secret entry, false otherwise - /// - /// - 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 buffer = Memory.UnsafeAlloc(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 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 stepBuffer = stackalloc byte[sizeof(long)]; - Span 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 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 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"; - - /// - /// Gets the plugins ambient if loaded, or loads it if required. This class will - /// be unloaded when the plugin us unloaded. - /// - /// - /// The ambient - /// - /// - /// - public static MFAConfig? GetMfaConfig(this PluginBase plugin) - { - static MFAConfig? LoadMfaConfig(PluginBase pbase) - { - //Try to get the configuration object - IReadOnlyDictionary? 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; } - } - - - /// - /// Gets the stored PGP public key for the user - /// - /// - /// The stored PGP signature key - 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 - - /// - /// Recovers a signed MFA upgrade JWT and verifies its authenticity, and confrims its not expired, - /// then recovers the upgrade mssage - /// - /// - /// The signed JWT upgrade message - /// The recovered upgrade - /// The stored base64 encoded signature from the session that requested an upgrade - /// True if the upgrade was verified, not expired, and was recovered from the signed message, false otherwise - public static bool RecoverUpgrade(this MFAConfig config, ReadOnlySpan upgradeJwtString, ReadOnlySpan base64sessionSig, [NotNullWhen(true)] out MFAUpgrade? upgrade) - { - //Verifies a jwt stored signature against the actual signature - static bool VerifyStoredSig(ReadOnlySpan base64string, ReadOnlySpan signature) - { - using UnsafeMemoryHandle buffer = Memory.UnsafeAlloc(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(); - return upgrade != null; - } - - - /// - /// Generates an upgrade for the requested user, using the highest prirotiy method - /// - /// The message from the user requesting the login - /// A signed upgrade message the client will pass back to the server after the MFA verification - /// - public static Tuple? 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 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]; - } -} -- cgit