From 52b8e30437e235817ed534dec860e781bb0468c0 Mon Sep 17 00:00:00 2001 From: vnugent Date: Sat, 14 Jan 2023 16:24:28 -0500 Subject: MemoryUtil native integer size update + tests --- .../src/Accounts/AccountManager.cs | 872 ------------------- .../src/Accounts/AccountUtils.cs | 922 +++++++++++++++++++++ lib/Plugins.Essentials/src/Accounts/INonce.cs | 42 - .../src/Accounts/ISecretProvider.cs | 49 ++ .../src/Accounts/NonceExtensions.cs | 75 ++ .../src/Accounts/PasswordHashing.cs | 50 +- .../src/Extensions/JsonResponse.cs | 9 +- lib/Plugins.Essentials/src/HttpEntity.cs | 8 + lib/Plugins.Essentials/src/Sessions/SessionInfo.cs | 2 +- 9 files changed, 1082 insertions(+), 947 deletions(-) delete mode 100644 lib/Plugins.Essentials/src/Accounts/AccountManager.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/AccountUtils.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/ISecretProvider.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/NonceExtensions.cs (limited to 'lib/Plugins.Essentials/src') diff --git a/lib/Plugins.Essentials/src/Accounts/AccountManager.cs b/lib/Plugins.Essentials/src/Accounts/AccountManager.cs deleted file mode 100644 index f148fdb..0000000 --- a/lib/Plugins.Essentials/src/Accounts/AccountManager.cs +++ /dev/null @@ -1,872 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials -* File: AccountManager.cs -* -* AccountManager.cs is part of VNLib.Plugins.Essentials which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials 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 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.IO; -using System.Text; -using System.Threading.Tasks; -using System.Security.Cryptography; -using System.Text.RegularExpressions; -using System.Runtime.CompilerServices; - -using VNLib.Hashing; -using VNLib.Net.Http; -using VNLib.Utils; -using VNLib.Utils.Memory; -using VNLib.Utils.Extensions; -using VNLib.Plugins.Essentials.Users; -using VNLib.Plugins.Essentials.Sessions; -using VNLib.Plugins.Essentials.Extensions; - - -#nullable enable - -namespace VNLib.Plugins.Essentials.Accounts -{ - - /// - /// Provides essential constants, static methods, and session/user extensions - /// to facilitate unified user-controls, athentication, and security - /// application-wide - /// - public static partial class AccountManager - { - public const int MAX_EMAIL_CHARS = 50; - public const int ID_FIELD_CHARS = 65; - public const int STREET_ADDR_CHARS = 150; - public const int MAX_LOGIN_COUNT = 10; - public const int MAX_FAILED_RESET_ATTEMPS = 5; - - /// - /// The maximum time in seconds for a login message to be considered valid - /// - public const double MAX_TIME_DIFF_SECS = 10.00; - /// - /// The size in bytes of the random passwords generated when invoking the - /// - public const int RANDOM_PASS_SIZE = 128; - /// - /// The name of the header that will identify a client's identiy - /// - public const string LOGIN_TOKEN_HEADER = "X-Web-Token"; - /// - /// The origin string of a local user account. This value will be set if an - /// account is created through the VNLib.Plugins.Essentials.Accounts library - /// - public const string LOCAL_ACCOUNT_ORIGIN = "local"; - /// - /// The size (in bytes) of the challenge secret - /// - public const int CHALLENGE_SIZE = 64; - /// - /// The size (in bytes) of the sesssion long user-password challenge - /// - public const int SESSION_CHALLENGE_SIZE = 128; - - //The buffer size to use when decoding the base64 public key from the user - private const int PUBLIC_KEY_BUFFER_SIZE = 1024; - /// - /// The name of the login cookie set when a user logs in - /// - public const string LOGIN_COOKIE_NAME = "VNLogin"; - /// - /// The name of the login client identifier cookie (cookie that is set fir client to use to determine if the user is logged in) - /// - public const string LOGIN_COOKIE_IDENTIFIER = "li"; - - private const int LOGIN_COOKIE_SIZE = 64; - - //Session entry keys - private const string BROWSER_ID_ENTRY = "acnt.bid"; - private const string CLIENT_PUB_KEY_ENTRY = "acnt.pbk"; - private const string CHALLENGE_HMAC_ENTRY = "acnt.cdig"; - private const string FAILED_LOGIN_ENTRY = "acnt.flc"; - private const string LOCAL_ACCOUNT_ENTRY = "acnt.ila"; - private const string ACC_ORIGIN_ENTRY = "__.org"; - //private const string CHALLENGE_HASH_ENTRY = "acnt.chl"; - - //Privlage masks - public const ulong READ_MSK = 0x0000000000000001L; - public const ulong DOWNLOAD_MSK = 0x0000000000000002L; - public const ulong WRITE_MSK = 0x0000000000000004L; - public const ulong DELETE_MSK = 0x0000000000000008L; - public const ulong ALLFILE_MSK = 0x000000000000000FL; - public const ulong OPTIONS_MSK = 0x000000000000FF00L; - public const ulong GROUP_MSK = 0x00000000FFFF0000L; - public const ulong LEVEL_MSK = 0x000000FF00000000L; - - public const byte OPTIONS_MSK_OFFSET = 0x08; - public const byte GROUP_MSK_OFFSET = 0x10; - public const byte LEVEL_MSK_OFFSET = 0x18; - - public const ulong MINIMUM_LEVEL = 0x0000000100000001L; - - //Timeouts - public static readonly TimeSpan LoginCookieLifespan = TimeSpan.FromHours(1); - public static readonly TimeSpan RegenIdPeriod = TimeSpan.FromMinutes(25); - - /// - /// The client data encryption padding. - /// - public static readonly RSAEncryptionPadding ClientEncryptonPadding = RSAEncryptionPadding.OaepSHA256; - - /// - /// The size (in bytes) of the web-token hash size - /// - private static readonly int TokenHashSize = (SHA384.Create().HashSize / 8); - - /// - /// Speical character regual expresion for basic checks - /// - public static readonly Regex SpecialCharacters = new(@"[\r\n\t\a\b\e\f#?!@$%^&*\+\-\~`|<>\{}]", RegexOptions.Compiled); - - #region Password/User helper extensions - - /// - /// Generates and sets a random password for the specified user account - /// - /// The configured to process the password update on - /// The user instance to update the password on - /// The instance to hash the random password with - /// Size (in bytes) of the generated random password - /// A value indicating the results of the event (number of rows affected, should evaluate to true) - /// - /// - /// - public static async Task SetRandomPasswordAsync(this PasswordHashing passHashing, IUserManager manager, IUser user, int size = RANDOM_PASS_SIZE) - { - _ = manager ?? throw new ArgumentNullException(nameof(manager)); - _ = user ?? throw new ArgumentNullException(nameof(user)); - _ = passHashing ?? throw new ArgumentNullException(nameof(passHashing)); - if (user.IsReleased) - { - throw new ObjectDisposedException("The specifed user object has been released"); - } - //Alloc a buffer - using IMemoryHandle buffer = Memory.SafeAlloc(size); - //Use the CGN to get a random set - RandomHash.GetRandomBytes(buffer.Span); - //Hash the new random password - using PrivateString passHash = passHashing.Hash(buffer.Span); - //Write the password to the user account - return await manager.UpdatePassAsync(user, passHash); - } - - - /// - /// Checks to see if the current user account was created - /// using a local account. - /// - /// - /// True if the account is a local account, false otherwise - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsLocalAccount(this IUser user) => LOCAL_ACCOUNT_ORIGIN.Equals(user.GetAccountOrigin(), StringComparison.Ordinal); - - /// - /// If this account was created by any means other than a local account creation. - /// Implementors can use this method to determine the origin of the account. - /// This field is not required - /// - /// The origin of the account - public static string GetAccountOrigin(this IUser ud) => ud[ACC_ORIGIN_ENTRY]; - /// - /// If this account was created by any means other than a local account creation. - /// Implementors can use this method to specify the origin of the account. This field is not required - /// - /// - /// Value of the account origin - public static void SetAccountOrigin(this IUser ud, string origin) => ud[ACC_ORIGIN_ENTRY] = origin; - - /// - /// Gets a random user-id generated from crypograhic random number - /// then hashed (SHA1) and returns a hexadecimal string - /// - /// The random string user-id - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetRandomUserId() => RandomHash.GetRandomHash(HashAlg.SHA1, 64, HashEncodingMode.Hexadecimal); - - #endregion - - #region Client Auth Extensions - - /// - /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables - /// - /// The connection and session to log-in - /// The message of the client to set the log-in status of - /// The user to log-in - /// The encrypted base64 token secret data to send to the client - /// - /// - public static string GenerateAuthorization(this HttpEntity ev, LoginMessage loginMessage, IUser user) - { - return GenerateAuthorization(ev, loginMessage.ClientPublicKey, loginMessage.ClientID, user); - } - - /// - /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables - /// - /// The connection and session to log-in - /// The clients base64 public key - /// The browser/client id - /// The user to log-in - /// The encrypted base64 token secret data to send to the client - /// - /// - /// - public static string GenerateAuthorization(this HttpEntity ev, string base64PubKey, string clientId, IUser user) - { - if (!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) - { - throw new InvalidOperationException("The session is not set or the session is not a web-based session type"); - } - //derrive token from login data - TryGenerateToken(base64PubKey, out string base64ServerToken, out string base64ClientData); - //Clear flags - user.FailedLoginCount(0); - //Get the "local" account flag from the user object - bool localAccount = user.IsLocalAccount(); - //Set login cookie and session login hash - ev.SetLogin(localAccount); - //Store variables - ev.Session.UserID = user.UserID; - ev.Session.Privilages = user.Privilages; - //Store browserid/client id if specified - SetBrowserID(in ev.Session, clientId); - //Store the clients public key - SetBrowserPubKey(in ev.Session, base64PubKey); - //Set local account flag - ev.Session.HasLocalAccount(localAccount); - //Store the base64 server key to compute the hmac later - ev.Session.Token = base64ServerToken; - //Return the client encrypted data - return base64ClientData; - } - - /* - * Notes for RSA client token generator code below - * - * To log-in a client with the following API the calling code - * must have already determined that the client should be - * logged in (verified passwords or auth tokens). - * - * The client will send a LoginMessage object that will - * contain the following Information. - * - * - The clients RSA public key in base64 subject-key info format - * - The client browser's id hex string - * - The clients local-time - * - * The TryGenerateToken method, will generate a random-byte token, - * encrypt it using the clients RSA public key, return the encrypted - * token data to the client, and only the client will be able to - * decrypt the token data. - * - * The token data is also hashed with SHA-256 (for future use) and - * stored in the client's session store. The client must decrypt - * the token data, hash it, and return it as a header for verification. - * - * Ideally the client should sign the data and send the signature or - * hash back, but it wont prevent MITM, and for now I think it just - * adds extra overhead for every connection during the HttpEvent.TokenMatches() - * check extension method - */ - - private ref struct TokenGenBuffers - { - public readonly Span Buffer { private get; init; } - public readonly Span SignatureBuffer => Buffer[..64]; - - - - public int ClientPbkWritten; - public readonly Span ClientPublicKeyBuffer => Buffer.Slice(64, 1024); - public readonly ReadOnlySpan ClientPbkOutput => ClientPublicKeyBuffer[..ClientPbkWritten]; - - - - public int ClientEncBytesWritten; - public readonly Span ClientEncOutputBuffer => Buffer[(64 + 1024)..]; - public readonly ReadOnlySpan EncryptedOutput => ClientEncOutputBuffer[..ClientEncBytesWritten]; - } - - /// - /// Computes a random buffer, encrypts it with the client's public key, - /// computes the digest of that key and returns the base64 encoded strings - /// of those components - /// - /// The user's public key credential - /// The base64 encoded digest of the secret that was encrypted - /// The client's user-agent header value - /// A string representing a unique signed token for a given login context - /// - /// - private static void TryGenerateToken(string base64clientPublicKey, out string base64Digest, out string base64ClientData) - { - //Temporary work buffer - using IMemoryHandle buffer = Memory.SafeAlloc(4096, true); - /* - * Create a new token buffer for bin buffers. - * This buffer struct is used to break up - * a single block of memory into individual - * non-overlapping (important!) buffer windows - * for named purposes - */ - TokenGenBuffers tokenBuf = new() - { - Buffer = buffer.Span - }; - //Recover the clients public key from its base64 encoding - if (!Convert.TryFromBase64String(base64clientPublicKey, tokenBuf.ClientPublicKeyBuffer, out tokenBuf.ClientPbkWritten)) - { - throw new InternalBufferOverflowException("Failed to recover the clients RSA public key"); - } - /* - * Fill signature buffer with random data - * this signature will be stored and used to verify - * signed client messages. It will also be encryped - * using the clients RSA keys - */ - RandomHash.GetRandomBytes(tokenBuf.SignatureBuffer); - /* - * Setup a new RSA Crypto provider that is initialized with the clients - * supplied public key. RSA will be used to encrypt the server secret - * that only the client will be able to decrypt for the current connection - */ - using RSA rsa = RSA.Create(); - //Setup rsa from the users public key - rsa.ImportSubjectPublicKeyInfo(tokenBuf.ClientPbkOutput, out _); - //try to encypte output data - if (!rsa.TryEncrypt(tokenBuf.SignatureBuffer, tokenBuf.ClientEncOutputBuffer, RSAEncryptionPadding.OaepSHA256, out tokenBuf.ClientEncBytesWritten)) - { - throw new InternalBufferOverflowException("Failed to encrypt the server secret"); - } - //Compute the digest of the raw server key - base64Digest = ManagedHash.ComputeBase64Hash(tokenBuf.SignatureBuffer, HashAlg.SHA384); - /* - * The client will send a hash of the decrypted key and will be used - * as a comparison to the hash string above ^ - */ - base64ClientData = Convert.ToBase64String(tokenBuf.EncryptedOutput, Base64FormattingOptions.None); - } - - /// - /// Determines if the client sent a token header, and it maches against the current session - /// - /// true if the client set the token header, the session is loaded, and the token matches the session, false otherwise - public static bool TokenMatches(this HttpEntity ev) - { - //Get the token from the client header, the client should always sent this - string? clientDigest = ev.Server.Headers[LOGIN_TOKEN_HEADER]; - //Make sure a session is loaded - if (!ev.Session.IsSet || ev.Session.IsNew || string.IsNullOrWhiteSpace(clientDigest)) - { - return false; - } - /* - * Alloc buffer to do conversion and zero initial contents incase the - * payload size has been changed. - * - * The buffer just needs to be large enoguh for the size of the hashes - * that are stored in base64 format. - * - * The values in the buffers will be the raw hash of the client's key - * and the stored key sent during initial authorziation. If the hashes - * are equal it should mean that the client must have the private - * key that generated the public key that was sent - */ - using UnsafeMemoryHandle buffer = Memory.UnsafeAlloc(TokenHashSize * 2, true); - //Slice up buffers - Span headerBuffer = buffer.Span[..TokenHashSize]; - Span sessionBuffer = buffer.Span[TokenHashSize..]; - //Convert the header token and the session token - if (Convert.TryFromBase64String(clientDigest, headerBuffer, out int headerTokenLen) - && Convert.TryFromBase64String(ev.Session.Token, sessionBuffer, out int sessionTokenLen)) - { - //Do a fixed time equal (probably overkill, but should not matter too much) - return CryptographicOperations.FixedTimeEquals(headerBuffer[..headerTokenLen], sessionBuffer[..sessionTokenLen]); - } - return false; - } - - /// - /// Regenerates the user's login token with the public key stored - /// during initial logon - /// - /// The base64 of the newly encrypted secret - public static string? RegenerateClientToken(this HttpEntity ev) - { - if(!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) - { - return null; - } - //Get the client's stored public key - string clientPublicKey = ev.Session.GetBrowserPubKey(); - //Make sure its set - if (string.IsNullOrWhiteSpace(clientPublicKey)) - { - return null; - } - //Generate a new token using the stored public key - TryGenerateToken(clientPublicKey, out string base64Digest, out string base64ClientData); - //store the token to the user's session - ev.Session.Token = base64Digest; - //return the clients encrypted secret - return base64ClientData; - } - - /// - /// Tries to encrypt the specified data using the stored public key and store the encrypted data into - /// the output buffer. - /// - /// - /// Data to encrypt - /// The buffer to store encrypted data in - /// - /// The number of encrypted bytes written to the output buffer, - /// or false (0) if the operation failed, or if no credential is - /// stored. - /// - /// - public static ERRNO TryEncryptClientData(this in SessionInfo session, ReadOnlySpan data, in Span outputBuffer) - { - if (!session.IsSet) - { - return false; - } - //try to get the public key from the client - string base64PubKey = session.GetBrowserPubKey(); - return TryEncryptClientData(base64PubKey, data, in outputBuffer); - } - /// - /// Tries to encrypt the specified data using the specified public key - /// - /// A base64 encoded public key used to encrypt client data - /// Data to encrypt - /// The buffer to store encrypted data in - /// - /// The number of encrypted bytes written to the output buffer, - /// or false (0) if the operation failed, or if no credential is - /// specified. - /// - /// - public static ERRNO TryEncryptClientData(ReadOnlySpan base64PubKey, ReadOnlySpan data, in Span outputBuffer) - { - if (base64PubKey.IsEmpty) - { - return false; - } - //Alloc a buffer for decoding the public key - using UnsafeMemoryHandle pubKeyBuffer = Memory.UnsafeAlloc(PUBLIC_KEY_BUFFER_SIZE, true); - //Decode the public key - ERRNO pbkBytesWritten = VnEncoding.TryFromBase64Chars(base64PubKey, pubKeyBuffer); - //Try to encrypt the data - return pbkBytesWritten ? TryEncryptClientData(pubKeyBuffer.Span[..(int)pbkBytesWritten], data, in outputBuffer) : false; - } - /// - /// Tries to encrypt the specified data using the specified public key - /// - /// The raw SKI public key - /// Data to encrypt - /// The buffer to store encrypted data in - /// - /// The number of encrypted bytes written to the output buffer, - /// or false (0) if the operation failed, or if no credential is - /// specified. - /// - /// - public static ERRNO TryEncryptClientData(ReadOnlySpan rawPubKey, ReadOnlySpan data, in Span outputBuffer) - { - if (rawPubKey.IsEmpty) - { - return false; - } - //Setup new empty rsa - using RSA rsa = RSA.Create(); - //Import the public key - rsa.ImportSubjectPublicKeyInfo(rawPubKey, out _); - //Encrypt data with OaepSha256 as configured in the browser - return rsa.TryEncrypt(data, outputBuffer, ClientEncryptonPadding, out int bytesWritten) ? bytesWritten : false; - } - - /// - /// Stores the clients public key specified during login - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetBrowserPubKey(in SessionInfo session, string base64PubKey) => session[CLIENT_PUB_KEY_ENTRY] = base64PubKey; - - /// - /// Gets the clients stored public key that was specified during login - /// - /// The base64 encoded public key string specified at login - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetBrowserPubKey(this in SessionInfo session) => session[CLIENT_PUB_KEY_ENTRY]; - - /// - /// Stores the login key as a cookie in the current session as long as the session exists - /// / - /// The event to log-in - /// Does the session belong to a local user account - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetLogin(this HttpEntity ev, bool? localAccount = null) - { - //Make sure the session is loaded - if (!ev.Session.IsSet) - { - return; - } - string loginString = RandomHash.GetRandomBase64(LOGIN_COOKIE_SIZE); - //Set login cookie and session login hash - ev.Server.SetCookie(LOGIN_COOKIE_NAME, loginString, "", "/", LoginCookieLifespan, CookieSameSite.SameSite, true, true); - ev.Session.LoginHash = loginString; - //If not set get from session storage - localAccount ??= ev.Session.HasLocalAccount(); - //Set the client identifier cookie to a value indicating a local account - ev.Server.SetCookie(LOGIN_COOKIE_IDENTIFIER, localAccount.Value ? "1" : "2", "", "/", LoginCookieLifespan, CookieSameSite.SameSite, false, true); - } - - /// - /// Invalidates the login status of the current connection and session (if session is loaded) - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InvalidateLogin(this HttpEntity ev) - { - //Expire the login cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite, secure: true); - //Expire the identifier cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); - if (ev.Session.IsSet) - { - //Invalidate the session - ev.Session.Invalidate(); - } - } - - /// - /// Determines if the current session login cookie matches the value stored in the current session (if the session is loaded) - /// - /// True if the session is active, the cookie was properly received, and the cookie value matches the session. False otherwise - public static bool LoginCookieMatches(this HttpEntity ev) - { - //Sessions must be loaded - if (!ev.Session.IsSet) - { - return false; - } - //Try to get the login string from the request cookies - if (!ev.Server.RequestCookies.TryGetNonEmptyValue(LOGIN_COOKIE_NAME, out string? liCookie)) - { - return false; - } - /* - * Alloc buffer to do conversion and zero initial contents incase the - * payload size has been changed. - * - * Since the cookie size and the local copy should be the same size - * and equal to the LOGIN_COOKIE_SIZE constant, the buffer size should - * be 2 * LOGIN_COOKIE_SIZE, and it can be split in half and shared - * for both conversions - */ - using UnsafeMemoryHandle buffer = Memory.UnsafeAlloc(2 * LOGIN_COOKIE_SIZE, true); - //Slice up buffers - Span cookieBuffer = buffer.Span[..LOGIN_COOKIE_SIZE]; - Span sessionBuffer = buffer.Span.Slice(LOGIN_COOKIE_SIZE, LOGIN_COOKIE_SIZE); - //Convert cookie and session hash value - if (Convert.TryFromBase64String(liCookie, cookieBuffer, out _) - && Convert.TryFromBase64String(ev.Session.LoginHash, sessionBuffer, out _)) - { - //Do a fixed time equal (probably overkill, but should not matter too much) - if(CryptographicOperations.FixedTimeEquals(cookieBuffer, sessionBuffer)) - { - //If the user is "logged in" and the request is using the POST method, then we can update the cookie - if(ev.Server.Method == HttpMethod.POST && ev.Session.Created.Add(RegenIdPeriod) < DateTimeOffset.UtcNow) - { - //Regen login token - ev.SetLogin(); - ev.Session.RegenID(); - } - - return true; - } - } - return false; - } - - /// - /// Determines if the client's login cookies need to be updated - /// to reflect its state with the current session's state - /// for the client - /// - /// - public static void ReconcileCookies(this HttpEntity ev) - { - //Only handle cookies if session is loaded and is a web based session - if (!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) - { - return; - } - if (ev.Session.IsNew) - { - //If either login cookies are set on a new session, clear them - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME) || ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) - { - //Expire the login cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite:CookieSameSite.SameSite, secure:true); - //Expire the identifier cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); - } - } - //If the session is not supposed to be logged in, clear the login cookies if they were set - else if (string.IsNullOrEmpty(ev.Session.LoginHash)) - { - //If one of either cookie is not set - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME)) - { - //Expire the login cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite, secure: true); - } - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) - { - //Expire the identifier cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); - } - } - } - - - /// - /// Stores the browser's id during a login process - /// - /// - /// Browser id value to store - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetBrowserID(in SessionInfo session, string browserId) => session[BROWSER_ID_ENTRY] = browserId; - - /// - /// Gets the current browser's id if it was specified during login process - /// - /// The browser's id if set, otherwise - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetBrowserID(this in SessionInfo session) => session[BROWSER_ID_ENTRY]; - - /// - /// Specifies that the current session belongs to a local user-account - /// - /// - /// True for a local account, false otherwise - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void HasLocalAccount(this in SessionInfo session, bool value) => session[LOCAL_ACCOUNT_ENTRY] = value ? "1" : null; - /// - /// Gets a value indicating if the session belongs to a local user account - /// - /// - /// True if the current user's account is a local account - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool HasLocalAccount(this in SessionInfo session) => int.TryParse(session[LOCAL_ACCOUNT_ENTRY], out int value) && value > 0; - - #endregion - - #region Client Challenge - - /* - * Generates a secret that is used to compute the unique hmac digest of the - * current user's password. The digest is stored in the current session - * and used to compare future requests that require password re-authentication. - * The client will compute the digest of the user's password and send the digest - * instead of the user's password - */ - - /// - /// Generates a new password challenge for the current session and specified password - /// - /// - /// The user's password to compute the hash of - /// The raw derrivation key to send to the client - public static byte[] GenPasswordChallenge(this in SessionInfo session, PrivateString password) - { - ReadOnlySpan rawPass = password; - //Calculate the password buffer size required - int passByteCount = Encoding.UTF8.GetByteCount(rawPass); - //Allocate the buffer - using UnsafeMemoryHandle bufferHandle = Memory.UnsafeAlloc(passByteCount + 64, true); - //Slice buffers - Span utf8PassBytes = bufferHandle.Span[..passByteCount]; - Span hashBuffer = bufferHandle.Span[passByteCount..]; - //Encode the password into the buffer - _ = Encoding.UTF8.GetBytes(rawPass, utf8PassBytes); - try - { - //Get random secret buffer - byte[] secretKey = RandomHash.GetRandomBytes(SESSION_CHALLENGE_SIZE); - //Compute the digest - int count = HMACSHA512.HashData(secretKey, utf8PassBytes, hashBuffer); - //Store the user's password digest - session[CHALLENGE_HMAC_ENTRY] = VnEncoding.ToBase32String(hashBuffer[..count], false); - return secretKey; - } - finally - { - //Wipe buffer - RandomHash.GetRandomBytes(utf8PassBytes); - } - } - /// - /// Verifies the stored unique digest of the user's password against - /// the client derrived password - /// - /// - /// The base64 client derrived digest of the user's password to verify - /// True if formatting was correct and the derrived passwords match, false otherwise - /// - public static bool VerifyChallenge(this in SessionInfo session, ReadOnlySpan base64PasswordDigest) - { - string base32Digest = session[CHALLENGE_HMAC_ENTRY]; - if (string.IsNullOrWhiteSpace(base32Digest)) - { - return false; - } - int bufSize = base32Digest.Length + base64PasswordDigest.Length; - //Alloc buffer - using UnsafeMemoryHandle buffer = Memory.UnsafeAlloc(bufSize); - //Split buffers - Span localBuf = buffer.Span[..base32Digest.Length]; - Span passBuf = buffer.Span[base32Digest.Length..]; - //Recover the stored base32 digest - ERRNO count = VnEncoding.TryFromBase32Chars(base32Digest, localBuf); - if (!count) - { - return false; - } - //Recover base64 bytes - if(!Convert.TryFromBase64Chars(base64PasswordDigest, passBuf, out int passBytesWritten)) - { - return false; - } - //Trim buffers - localBuf = localBuf[..(int)count]; - passBuf = passBuf[..passBytesWritten]; - //Compare and return - return CryptographicOperations.FixedTimeEquals(passBuf, localBuf); - } - - #endregion - - #region Privilage Extensions - /// - /// Compares the users privilage level against the specified level - /// - /// - /// 64bit privilage level to compare - /// true if the current user has at least the specified level or higher - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool HasLevel(this in SessionInfo session, byte level) => (session.Privilages & LEVEL_MSK) >= (((ulong)level << LEVEL_MSK_OFFSET) & LEVEL_MSK); - /// - /// Determines if the group ID of the current user matches the specified group - /// - /// - /// Group ID to compare - /// true if the user belongs to the group, false otherwise - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool HasGroup(this in SessionInfo session, ushort groupId) => (session.Privilages & GROUP_MSK) == (((ulong)groupId << GROUP_MSK_OFFSET) & GROUP_MSK); - /// - /// Determines if the current user has an equivalent option code - /// - /// - /// Option code check - /// true if the user options field equals the option - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool HasOption(this in SessionInfo session, byte option) => (session.Privilages & OPTIONS_MSK) == (((ulong)option << OPTIONS_MSK_OFFSET) & OPTIONS_MSK); - - /// - /// Returns the status of the user's privlage read bit - /// - /// true if the current user has the read permission, false otherwise - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CanRead(this in SessionInfo session) => (session.Privilages & READ_MSK) == READ_MSK; - /// - /// Returns the status of the user's privlage write bit - /// - /// true if the current user has the write permission, false otherwise - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CanWrite(this in SessionInfo session) => (session.Privilages & WRITE_MSK) == WRITE_MSK; - /// - /// Returns the status of the user's privlage delete bit - /// - /// true if the current user has the delete permission, false otherwise - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CanDelete(this in SessionInfo session) => (session.Privilages & DELETE_MSK) == DELETE_MSK; - #endregion - - #region flc - - /// - /// Gets the current number of failed login attempts - /// - /// - /// The current number of failed login attempts - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TimestampedCounter FailedLoginCount(this IUser user) - { - ulong value = user.GetValueType(FAILED_LOGIN_ENTRY); - return (TimestampedCounter)value; - } - /// - /// Sets the number of failed login attempts for the current session - /// - /// - /// The value to set the failed login attempt count - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void FailedLoginCount(this IUser user, uint value) - { - TimestampedCounter counter = new(value); - //Cast the counter to a ulong and store as a ulong - user.SetValueType(FAILED_LOGIN_ENTRY, (ulong)counter); - } - /// - /// Sets the number of failed login attempts for the current session - /// - /// - /// The value to set the failed login attempt count - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void FailedLoginCount(this IUser user, TimestampedCounter value) - { - //Cast the counter to a ulong and store as a ulong - user.SetValueType(FAILED_LOGIN_ENTRY, (ulong)value); - } - /// - /// Increments the failed login attempt count - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void FailedLoginIncrement(this IUser user) - { - TimestampedCounter current = user.FailedLoginCount(); - user.FailedLoginCount(current.Count + 1); - } - - #endregion - } -} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs new file mode 100644 index 0000000..610d646 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs @@ -0,0 +1,922 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: AccountManager.cs +* +* AccountManager.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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 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.IO; +using System.Text; +using System.Threading.Tasks; +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using System.Runtime.CompilerServices; + +using VNLib.Hashing; +using VNLib.Net.Http; +using VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; +using VNLib.Plugins.Essentials.Users; +using VNLib.Plugins.Essentials.Sessions; +using VNLib.Plugins.Essentials.Extensions; + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + + /// + /// Provides essential constants, static methods, and session/user extensions + /// to facilitate unified user-controls, athentication, and security + /// application-wide + /// + public static partial class AccountUtil + { + public const int MAX_EMAIL_CHARS = 50; + public const int ID_FIELD_CHARS = 65; + public const int STREET_ADDR_CHARS = 150; + public const int MAX_LOGIN_COUNT = 10; + public const int MAX_FAILED_RESET_ATTEMPS = 5; + + /// + /// The maximum time in seconds for a login message to be considered valid + /// + public const double MAX_TIME_DIFF_SECS = 10.00; + /// + /// The size in bytes of the random passwords generated when invoking the + /// + public const int RANDOM_PASS_SIZE = 128; + /// + /// The name of the header that will identify a client's identiy + /// + public const string LOGIN_TOKEN_HEADER = "X-Web-Token"; + /// + /// The origin string of a local user account. This value will be set if an + /// account is created through the VNLib.Plugins.Essentials.Accounts library + /// + public const string LOCAL_ACCOUNT_ORIGIN = "local"; + /// + /// The size (in bytes) of the challenge secret + /// + public const int CHALLENGE_SIZE = 64; + /// + /// The size (in bytes) of the sesssion long user-password challenge + /// + public const int SESSION_CHALLENGE_SIZE = 128; + + //The buffer size to use when decoding the base64 public key from the user + private const int PUBLIC_KEY_BUFFER_SIZE = 1024; + /// + /// The name of the login cookie set when a user logs in + /// + public const string LOGIN_COOKIE_NAME = "VNLogin"; + /// + /// The name of the login client identifier cookie (cookie that is set fir client to use to determine if the user is logged in) + /// + public const string LOGIN_COOKIE_IDENTIFIER = "li"; + + private const int LOGIN_COOKIE_SIZE = 64; + + //Session entry keys + private const string BROWSER_ID_ENTRY = "acnt.bid"; + private const string CLIENT_PUB_KEY_ENTRY = "acnt.pbk"; + private const string CHALLENGE_HMAC_ENTRY = "acnt.cdig"; + private const string FAILED_LOGIN_ENTRY = "acnt.flc"; + private const string LOCAL_ACCOUNT_ENTRY = "acnt.ila"; + private const string ACC_ORIGIN_ENTRY = "__.org"; + private const string TOKEN_UPDATE_TIME_ENTRY = "acnt.tut"; + //private const string CHALLENGE_HASH_ENTRY = "acnt.chl"; + + //Privlage masks + public const ulong READ_MSK = 0x0000000000000001L; + public const ulong DOWNLOAD_MSK = 0x0000000000000002L; + public const ulong WRITE_MSK = 0x0000000000000004L; + public const ulong DELETE_MSK = 0x0000000000000008L; + public const ulong ALLFILE_MSK = 0x000000000000000FL; + public const ulong OPTIONS_MSK = 0x000000000000FF00L; + public const ulong GROUP_MSK = 0x00000000FFFF0000L; + public const ulong LEVEL_MSK = 0x000000FF00000000L; + + public const byte OPTIONS_MSK_OFFSET = 0x08; + public const byte GROUP_MSK_OFFSET = 0x10; + public const byte LEVEL_MSK_OFFSET = 0x18; + + public const ulong MINIMUM_LEVEL = 0x0000000100000001L; + + //Timeouts + public static readonly TimeSpan LoginCookieLifespan = TimeSpan.FromHours(1); + public static readonly TimeSpan RegenIdPeriod = TimeSpan.FromMinutes(25); + + /// + /// The client data encryption padding. + /// + public static readonly RSAEncryptionPadding ClientEncryptonPadding = RSAEncryptionPadding.OaepSHA256; + + /// + /// The size (in bytes) of the web-token hash size + /// + private static readonly int TokenHashSize = (SHA384.Create().HashSize / 8); + + /// + /// Speical character regual expresion for basic checks + /// + public static readonly Regex SpecialCharacters = new(@"[\r\n\t\a\b\e\f#?!@$%^&*\+\-\~`|<>\{}]", RegexOptions.Compiled); + + #region Password/User helper extensions + + /// + /// Generates and sets a random password for the specified user account + /// + /// The configured to process the password update on + /// The user instance to update the password on + /// The instance to hash the random password with + /// Size (in bytes) of the generated random password + /// A value indicating the results of the event (number of rows affected, should evaluate to true) + /// + /// + /// + public static async Task SetRandomPasswordAsync(this PasswordHashing passHashing, IUserManager manager, IUser user, int size = RANDOM_PASS_SIZE) + { + _ = manager ?? throw new ArgumentNullException(nameof(manager)); + _ = user ?? throw new ArgumentNullException(nameof(user)); + _ = passHashing ?? throw new ArgumentNullException(nameof(passHashing)); + if (user.IsReleased) + { + throw new ObjectDisposedException("The specifed user object has been released"); + } + //Alloc a buffer + using IMemoryHandle buffer = MemoryUtil.SafeAlloc(size); + //Use the CGN to get a random set + RandomHash.GetRandomBytes(buffer.Span); + //Hash the new random password + using PrivateString passHash = passHashing.Hash(buffer.Span); + //Write the password to the user account + return await manager.UpdatePassAsync(user, passHash); + } + + + /// + /// Checks to see if the current user account was created + /// using a local account. + /// + /// + /// True if the account is a local account, false otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLocalAccount(this IUser user) => LOCAL_ACCOUNT_ORIGIN.Equals(user.GetAccountOrigin(), StringComparison.Ordinal); + + /// + /// If this account was created by any means other than a local account creation. + /// Implementors can use this method to determine the origin of the account. + /// This field is not required + /// + /// The origin of the account + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetAccountOrigin(this IUser ud) => ud[ACC_ORIGIN_ENTRY]; + /// + /// If this account was created by any means other than a local account creation. + /// Implementors can use this method to specify the origin of the account. This field is not required + /// + /// + /// Value of the account origin + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetAccountOrigin(this IUser ud, string origin) => ud[ACC_ORIGIN_ENTRY] = origin; + + /// + /// Gets a random user-id generated from crypograhic random number + /// then hashed (SHA1) and returns a hexadecimal string + /// + /// The random string user-id + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetRandomUserId() => RandomHash.GetRandomHash(HashAlg.SHA1, 64, HashEncodingMode.Hexadecimal); + + #endregion + + #region Client Auth Extensions + + /// + /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables + /// + /// The connection and session to log-in + /// The message of the client to set the log-in status of + /// The user to log-in + /// The encrypted base64 token secret data to send to the client + /// + /// + public static string GenerateAuthorization(this HttpEntity ev, LoginMessage loginMessage, IUser user) + { + return GenerateAuthorization(ev, loginMessage.ClientPublicKey, loginMessage.ClientID, user); + } + + /// + /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables + /// + /// The connection and session to log-in + /// The clients base64 public key + /// The browser/client id + /// The user to log-in + /// The encrypted base64 token secret data to send to the client + /// + /// + /// + public static string GenerateAuthorization(this HttpEntity ev, string base64PubKey, string clientId, IUser user) + { + if (!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) + { + throw new InvalidOperationException("The session is not set or the session is not a web-based session type"); + } + //Update session-id for "upgrade" + ev.Session.RegenID(); + //derrive token from login data + TryGenerateToken(base64PubKey, out string base64ServerToken, out string base64ClientData); + //Clear flags + user.FailedLoginCount(0); + //Get the "local" account flag from the user object + bool localAccount = user.IsLocalAccount(); + //Set login cookie and session login hash + ev.SetLogin(localAccount); + //Store variables + ev.Session.UserID = user.UserID; + ev.Session.Privilages = user.Privilages; + //Store browserid/client id if specified + SetBrowserID(in ev.Session, clientId); + //Store the clients public key + SetBrowserPubKey(in ev.Session, base64PubKey); + //Set local account flag + ev.Session.HasLocalAccount(localAccount); + //Store the base64 server key to compute the hmac later + ev.Session.Token = base64ServerToken; + //Update the last token upgrade time + ev.Session.LastTokenUpgrade(ev.RequestedTimeUtc); + //Return the client encrypted data + return base64ClientData; + } + + /* + * Notes for RSA client token generator code below + * + * To log-in a client with the following API the calling code + * must have already determined that the client should be + * logged in (verified passwords or auth tokens). + * + * The client will send a LoginMessage object that will + * contain the following Information. + * + * - The clients RSA public key in base64 subject-key info format + * - The client browser's id hex string + * - The clients local-time + * + * The TryGenerateToken method, will generate a random-byte token, + * encrypt it using the clients RSA public key, return the encrypted + * token data to the client, and only the client will be able to + * decrypt the token data. + * + * The token data is also hashed with SHA-256 (for future use) and + * stored in the client's session store. The client must decrypt + * the token data, hash it, and return it as a header for verification. + * + * Ideally the client should sign the data and send the signature or + * hash back, but it wont prevent MITM, and for now I think it just + * adds extra overhead for every connection during the HttpEvent.TokenMatches() + * check extension method + */ + + private ref struct TokenGenBuffers + { + public readonly Span Buffer { private get; init; } + public readonly Span SignatureBuffer => Buffer[..64]; + + + + public int ClientPbkWritten; + public readonly Span ClientPublicKeyBuffer => Buffer.Slice(64, 1024); + public readonly ReadOnlySpan ClientPbkOutput => ClientPublicKeyBuffer[..ClientPbkWritten]; + + + + public int ClientEncBytesWritten; + public readonly Span ClientEncOutputBuffer => Buffer[(64 + 1024)..]; + public readonly ReadOnlySpan EncryptedOutput => ClientEncOutputBuffer[..ClientEncBytesWritten]; + } + + /// + /// Computes a random buffer, encrypts it with the client's public key, + /// computes the digest of that key and returns the base64 encoded strings + /// of those components + /// + /// The user's public key credential + /// The base64 encoded digest of the secret that was encrypted + /// The client's user-agent header value + /// A string representing a unique signed token for a given login context + /// + /// + private static void TryGenerateToken(string base64clientPublicKey, out string base64Digest, out string base64ClientData) + { + //Temporary work buffer + using IMemoryHandle buffer = MemoryUtil.SafeAlloc(4096, true); + /* + * Create a new token buffer for bin buffers. + * This buffer struct is used to break up + * a single block of memory into individual + * non-overlapping (important!) buffer windows + * for named purposes + */ + TokenGenBuffers tokenBuf = new() + { + Buffer = buffer.Span + }; + //Recover the clients public key from its base64 encoding + if (!Convert.TryFromBase64String(base64clientPublicKey, tokenBuf.ClientPublicKeyBuffer, out tokenBuf.ClientPbkWritten)) + { + throw new InternalBufferOverflowException("Failed to recover the clients RSA public key"); + } + /* + * Fill signature buffer with random data + * this signature will be stored and used to verify + * signed client messages. It will also be encryped + * using the clients RSA keys + */ + RandomHash.GetRandomBytes(tokenBuf.SignatureBuffer); + /* + * Setup a new RSA Crypto provider that is initialized with the clients + * supplied public key. RSA will be used to encrypt the server secret + * that only the client will be able to decrypt for the current connection + */ + using RSA rsa = RSA.Create(); + //Setup rsa from the users public key + rsa.ImportSubjectPublicKeyInfo(tokenBuf.ClientPbkOutput, out _); + //try to encypte output data + if (!rsa.TryEncrypt(tokenBuf.SignatureBuffer, tokenBuf.ClientEncOutputBuffer, RSAEncryptionPadding.OaepSHA256, out tokenBuf.ClientEncBytesWritten)) + { + throw new InternalBufferOverflowException("Failed to encrypt the server secret"); + } + //Compute the digest of the raw server key + base64Digest = ManagedHash.ComputeBase64Hash(tokenBuf.SignatureBuffer, HashAlg.SHA384); + /* + * The client will send a hash of the decrypted key and will be used + * as a comparison to the hash string above ^ + */ + base64ClientData = Convert.ToBase64String(tokenBuf.EncryptedOutput, Base64FormattingOptions.None); + } + + /// + /// Determines if the client sent a token header, and it maches against the current session + /// + /// true if the client set the token header, the session is loaded, and the token matches the session, false otherwise + public static bool TokenMatches(this HttpEntity ev) + { + //Get the token from the client header, the client should always sent this + string? clientDigest = ev.Server.Headers[LOGIN_TOKEN_HEADER]; + //Make sure a session is loaded + if (!ev.Session.IsSet || ev.Session.IsNew || string.IsNullOrWhiteSpace(clientDigest)) + { + return false; + } + /* + * Alloc buffer to do conversion and zero initial contents incase the + * payload size has been changed. + * + * The buffer just needs to be large enoguh for the size of the hashes + * that are stored in base64 format. + * + * The values in the buffers will be the raw hash of the client's key + * and the stored key sent during initial authorziation. If the hashes + * are equal it should mean that the client must have the private + * key that generated the public key that was sent + */ + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(TokenHashSize * 2, true); + //Slice up buffers + Span headerBuffer = buffer.Span[..TokenHashSize]; + Span sessionBuffer = buffer.Span[TokenHashSize..]; + //Convert the header token and the session token + if (Convert.TryFromBase64String(clientDigest, headerBuffer, out int headerTokenLen) + && Convert.TryFromBase64String(ev.Session.Token, sessionBuffer, out int sessionTokenLen)) + { + //Do a fixed time equal (probably overkill, but should not matter too much) + if(CryptographicOperations.FixedTimeEquals(headerBuffer[..headerTokenLen], sessionBuffer[..sessionTokenLen])) + { + return true; + } + } + + /* + * If the token does not match, or cannot be found, check if the client + * has login cookies set, if not remove them. + * + * This does not affect the session, but allows for a web client to update + * its login state if its no-longer logged in + */ + + //Expire login cookie if set + if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME)) + { + ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite); + } + //Expire the LI cookie if set + if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) + { + ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite); + } + + return false; + } + + /// + /// Regenerates the user's login token with the public key stored + /// during initial logon + /// + /// The base64 of the newly encrypted secret + public static string? RegenerateClientToken(this HttpEntity ev) + { + if(!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) + { + return null; + } + //Get the client's stored public key + string clientPublicKey = ev.Session.GetBrowserPubKey(); + //Make sure its set + if (string.IsNullOrWhiteSpace(clientPublicKey)) + { + return null; + } + //Generate a new token using the stored public key + TryGenerateToken(clientPublicKey, out string base64Digest, out string base64ClientData); + //store the token to the user's session + ev.Session.Token = base64Digest; + //Update the last token upgrade time + ev.Session.LastTokenUpgrade(ev.RequestedTimeUtc); + //return the clients encrypted secret + return base64ClientData; + } + + /// + /// Tries to encrypt the specified data using the stored public key and store the encrypted data into + /// the output buffer. + /// + /// + /// Data to encrypt + /// The buffer to store encrypted data in + /// + /// The number of encrypted bytes written to the output buffer, + /// or false (0) if the operation failed, or if no credential is + /// stored. + /// + /// + public static ERRNO TryEncryptClientData(this in SessionInfo session, ReadOnlySpan data, in Span outputBuffer) + { + if (!session.IsSet) + { + return false; + } + //try to get the public key from the client + string base64PubKey = session.GetBrowserPubKey(); + return TryEncryptClientData(base64PubKey, data, in outputBuffer); + } + /// + /// Tries to encrypt the specified data using the specified public key + /// + /// A base64 encoded public key used to encrypt client data + /// Data to encrypt + /// The buffer to store encrypted data in + /// + /// The number of encrypted bytes written to the output buffer, + /// or false (0) if the operation failed, or if no credential is + /// specified. + /// + /// + public static ERRNO TryEncryptClientData(ReadOnlySpan base64PubKey, ReadOnlySpan data, in Span outputBuffer) + { + if (base64PubKey.IsEmpty) + { + return false; + } + //Alloc a buffer for decoding the public key + using UnsafeMemoryHandle pubKeyBuffer = MemoryUtil.UnsafeAlloc(PUBLIC_KEY_BUFFER_SIZE, true); + //Decode the public key + ERRNO pbkBytesWritten = VnEncoding.TryFromBase64Chars(base64PubKey, pubKeyBuffer); + //Try to encrypt the data + return pbkBytesWritten ? TryEncryptClientData(pubKeyBuffer.Span[..(int)pbkBytesWritten], data, in outputBuffer) : false; + } + /// + /// Tries to encrypt the specified data using the specified public key + /// + /// The raw SKI public key + /// Data to encrypt + /// The buffer to store encrypted data in + /// + /// The number of encrypted bytes written to the output buffer, + /// or false (0) if the operation failed, or if no credential is + /// specified. + /// + /// + public static ERRNO TryEncryptClientData(ReadOnlySpan rawPubKey, ReadOnlySpan data, in Span outputBuffer) + { + if (rawPubKey.IsEmpty) + { + return false; + } + //Setup new empty rsa + using RSA rsa = RSA.Create(); + //Import the public key + rsa.ImportSubjectPublicKeyInfo(rawPubKey, out _); + //Encrypt data with OaepSha256 as configured in the browser + return rsa.TryEncrypt(data, outputBuffer, ClientEncryptonPadding, out int bytesWritten) ? bytesWritten : false; + } + + /// + /// Stores the clients public key specified during login + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetBrowserPubKey(in SessionInfo session, string base64PubKey) => session[CLIENT_PUB_KEY_ENTRY] = base64PubKey; + + /// + /// Gets the clients stored public key that was specified during login + /// + /// The base64 encoded public key string specified at login + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetBrowserPubKey(this in SessionInfo session) => session[CLIENT_PUB_KEY_ENTRY]; + + /// + /// Stores the login key as a cookie in the current session as long as the session exists + /// / + /// The event to log-in + /// Does the session belong to a local user account + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetLogin(this HttpEntity ev, bool? localAccount = null) + { + //Make sure the session is loaded + if (!ev.Session.IsSet) + { + return; + } + string loginString = RandomHash.GetRandomBase64(LOGIN_COOKIE_SIZE); + //Set login cookie and session login hash + ev.Server.SetCookie(LOGIN_COOKIE_NAME, loginString, "", "/", LoginCookieLifespan, CookieSameSite.SameSite, true, true); + ev.Session.LoginHash = loginString; + //If not set get from session storage + localAccount ??= ev.Session.HasLocalAccount(); + //Set the client identifier cookie to a value indicating a local account + ev.Server.SetCookie(LOGIN_COOKIE_IDENTIFIER, localAccount.Value ? "1" : "2", "", "/", LoginCookieLifespan, CookieSameSite.SameSite, false, true); + } + + /// + /// Invalidates the login status of the current connection and session (if session is loaded) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void InvalidateLogin(this HttpEntity ev) + { + //Expire the login cookie + ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite, secure: true); + //Expire the identifier cookie + ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); + if (ev.Session.IsSet) + { + //Invalidate the session + ev.Session.Invalidate(); + } + } + + /// + /// Determines if the current session login cookie matches the value stored in the current session (if the session is loaded) + /// + /// True if the session is active, the cookie was properly received, and the cookie value matches the session. False otherwise + public static bool LoginCookieMatches(this HttpEntity ev) + { + //Sessions must be loaded + if (!ev.Session.IsSet) + { + return false; + } + //Try to get the login string from the request cookies + if (!ev.Server.RequestCookies.TryGetNonEmptyValue(LOGIN_COOKIE_NAME, out string? liCookie)) + { + return false; + } + /* + * Alloc buffer to do conversion and zero initial contents incase the + * payload size has been changed. + * + * Since the cookie size and the local copy should be the same size + * and equal to the LOGIN_COOKIE_SIZE constant, the buffer size should + * be 2 * LOGIN_COOKIE_SIZE, and it can be split in half and shared + * for both conversions + */ + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(2 * LOGIN_COOKIE_SIZE, true); + //Slice up buffers + Span cookieBuffer = buffer.Span[..LOGIN_COOKIE_SIZE]; + Span sessionBuffer = buffer.Span.Slice(LOGIN_COOKIE_SIZE, LOGIN_COOKIE_SIZE); + //Convert cookie and session hash value + if (Convert.TryFromBase64String(liCookie, cookieBuffer, out _) + && Convert.TryFromBase64String(ev.Session.LoginHash, sessionBuffer, out _)) + { + //Do a fixed time equal (probably overkill, but should not matter too much) + if(CryptographicOperations.FixedTimeEquals(cookieBuffer, sessionBuffer)) + { + //If the user is "logged in" and the request is using the POST method, then we can update the cookie + if(ev.Server.Method == HttpMethod.POST && ev.Session.Created.Add(RegenIdPeriod) < ev.RequestedTimeUtc) + { + //Regen login token + ev.SetLogin(); + ev.Session.RegenID(); + } + + return true; + } + } + return false; + } + + /// + /// Determines if the client's login cookies need to be updated + /// to reflect its state with the current session's state + /// for the client + /// + /// + public static void ReconcileCookies(this HttpEntity ev) + { + //Only handle cookies if session is loaded and is a web based session + if (!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) + { + return; + } + if (ev.Session.IsNew) + { + //If either login cookies are set on a new session, clear them + if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME) || ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) + { + //Expire the login cookie + ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite:CookieSameSite.SameSite, secure:true); + //Expire the identifier cookie + ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); + } + } + //If the session is not supposed to be logged in, clear the login cookies if they were set + else if (string.IsNullOrEmpty(ev.Session.LoginHash)) + { + //If one of either cookie is not set + if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME)) + { + //Expire the login cookie + ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite, secure: true); + } + if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) + { + //Expire the identifier cookie + ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); + } + } + } + + /// + /// Gets the last time the session token was set + /// + /// + /// The last time the token was updated/generated, or if not set + public static DateTimeOffset LastTokenUpgrade(this in SessionInfo session) + { + //Get the serialized time value + string timeString = session[TOKEN_UPDATE_TIME_ENTRY]; + return long.TryParse(timeString, out long time) ? DateTimeOffset.FromUnixTimeSeconds(time) : DateTimeOffset.MinValue; + } + + /// + /// Updates the last time the session token was set + /// + /// + /// The UTC time the last token was set + private static void LastTokenUpgrade(this in SessionInfo session, DateTimeOffset updated) + => session[TOKEN_UPDATE_TIME_ENTRY] = updated.ToUnixTimeSeconds().ToString(); + + /// + /// Stores the browser's id during a login process + /// + /// + /// Browser id value to store + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetBrowserID(in SessionInfo session, string browserId) => session[BROWSER_ID_ENTRY] = browserId; + + /// + /// Gets the current browser's id if it was specified during login process + /// + /// The browser's id if set, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string GetBrowserID(this in SessionInfo session) => session[BROWSER_ID_ENTRY]; + + /// + /// Specifies that the current session belongs to a local user-account + /// + /// + /// True for a local account, false otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void HasLocalAccount(this in SessionInfo session, bool value) => session[LOCAL_ACCOUNT_ENTRY] = value ? "1" : null; + /// + /// Gets a value indicating if the session belongs to a local user account + /// + /// + /// True if the current user's account is a local account + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasLocalAccount(this in SessionInfo session) => int.TryParse(session[LOCAL_ACCOUNT_ENTRY], out int value) && value > 0; + + #endregion + + #region Client Challenge + + /* + * Generates a secret that is used to compute the unique hmac digest of the + * current user's password. The digest is stored in the current session + * and used to compare future requests that require password re-authentication. + * The client will compute the digest of the user's password and send the digest + * instead of the user's password + */ + + /// + /// Generates a new password challenge for the current session and specified password + /// + /// + /// The user's password to compute the hash of + /// The raw derrivation key to send to the client + public static byte[] GenPasswordChallenge(this in SessionInfo session, PrivateString password) + { + ReadOnlySpan rawPass = password; + //Calculate the password buffer size required + int passByteCount = Encoding.UTF8.GetByteCount(rawPass); + //Allocate the buffer + using UnsafeMemoryHandle bufferHandle = MemoryUtil.UnsafeAlloc(passByteCount + 64, true); + //Slice buffers + Span utf8PassBytes = bufferHandle.Span[..passByteCount]; + Span hashBuffer = bufferHandle.Span[passByteCount..]; + //Encode the password into the buffer + _ = Encoding.UTF8.GetBytes(rawPass, utf8PassBytes); + try + { + //Get random secret buffer + byte[] secretKey = RandomHash.GetRandomBytes(SESSION_CHALLENGE_SIZE); + //Compute the digest + int count = HMACSHA512.HashData(secretKey, utf8PassBytes, hashBuffer); + //Store the user's password digest + session[CHALLENGE_HMAC_ENTRY] = VnEncoding.ToBase32String(hashBuffer[..count], false); + return secretKey; + } + finally + { + //Wipe buffer + RandomHash.GetRandomBytes(utf8PassBytes); + } + } + /// + /// Verifies the stored unique digest of the user's password against + /// the client derrived password + /// + /// + /// The base64 client derrived digest of the user's password to verify + /// True if formatting was correct and the derrived passwords match, false otherwise + /// + public static bool VerifyChallenge(this in SessionInfo session, ReadOnlySpan base64PasswordDigest) + { + string base32Digest = session[CHALLENGE_HMAC_ENTRY]; + if (string.IsNullOrWhiteSpace(base32Digest)) + { + return false; + } + int bufSize = base32Digest.Length + base64PasswordDigest.Length; + //Alloc buffer + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(bufSize); + //Split buffers + Span localBuf = buffer.Span[..base32Digest.Length]; + Span passBuf = buffer.Span[base32Digest.Length..]; + //Recover the stored base32 digest + ERRNO count = VnEncoding.TryFromBase32Chars(base32Digest, localBuf); + if (!count) + { + return false; + } + //Recover base64 bytes + if(!Convert.TryFromBase64Chars(base64PasswordDigest, passBuf, out int passBytesWritten)) + { + return false; + } + //Trim buffers + localBuf = localBuf[..(int)count]; + passBuf = passBuf[..passBytesWritten]; + //Compare and return + return CryptographicOperations.FixedTimeEquals(passBuf, localBuf); + } + + #endregion + + #region Privilage Extensions + /// + /// Compares the users privilage level against the specified level + /// + /// + /// 64bit privilage level to compare + /// true if the current user has at least the specified level or higher + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasLevel(this in SessionInfo session, byte level) => (session.Privilages & LEVEL_MSK) >= (((ulong)level << LEVEL_MSK_OFFSET) & LEVEL_MSK); + /// + /// Determines if the group ID of the current user matches the specified group + /// + /// + /// Group ID to compare + /// true if the user belongs to the group, false otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasGroup(this in SessionInfo session, ushort groupId) => (session.Privilages & GROUP_MSK) == (((ulong)groupId << GROUP_MSK_OFFSET) & GROUP_MSK); + /// + /// Determines if the current user has an equivalent option code + /// + /// + /// Option code check + /// true if the user options field equals the option + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasOption(this in SessionInfo session, byte option) => (session.Privilages & OPTIONS_MSK) == (((ulong)option << OPTIONS_MSK_OFFSET) & OPTIONS_MSK); + + /// + /// Returns the status of the user's privlage read bit + /// + /// true if the current user has the read permission, false otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanRead(this in SessionInfo session) => (session.Privilages & READ_MSK) == READ_MSK; + /// + /// Returns the status of the user's privlage write bit + /// + /// true if the current user has the write permission, false otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanWrite(this in SessionInfo session) => (session.Privilages & WRITE_MSK) == WRITE_MSK; + /// + /// Returns the status of the user's privlage delete bit + /// + /// true if the current user has the delete permission, false otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CanDelete(this in SessionInfo session) => (session.Privilages & DELETE_MSK) == DELETE_MSK; + #endregion + + #region flc + + /// + /// Gets the current number of failed login attempts + /// + /// + /// The current number of failed login attempts + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TimestampedCounter FailedLoginCount(this IUser user) + { + ulong value = user.GetValueType(FAILED_LOGIN_ENTRY); + return (TimestampedCounter)value; + } + /// + /// Sets the number of failed login attempts for the current session + /// + /// + /// The value to set the failed login attempt count + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FailedLoginCount(this IUser user, uint value) + { + TimestampedCounter counter = new(value); + //Cast the counter to a ulong and store as a ulong + user.SetValueType(FAILED_LOGIN_ENTRY, (ulong)counter); + } + /// + /// Sets the number of failed login attempts for the current session + /// + /// + /// The value to set the failed login attempt count + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FailedLoginCount(this IUser user, TimestampedCounter value) + { + //Cast the counter to a ulong and store as a ulong + user.SetValueType(FAILED_LOGIN_ENTRY, (ulong)value); + } + /// + /// Increments the failed login attempt count + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void FailedLoginIncrement(this IUser user) + { + TimestampedCounter current = user.FailedLoginCount(); + user.FailedLoginCount(current.Count + 1); + } + + #endregion + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/INonce.cs b/lib/Plugins.Essentials/src/Accounts/INonce.cs index 7d53183..3a1b779 100644 --- a/lib/Plugins.Essentials/src/Accounts/INonce.cs +++ b/lib/Plugins.Essentials/src/Accounts/INonce.cs @@ -24,9 +24,6 @@ using System; -using VNLib.Utils; -using VNLib.Utils.Memory; - namespace VNLib.Plugins.Essentials.Accounts { /// @@ -48,43 +45,4 @@ namespace VNLib.Plugins.Essentials.Accounts /// True if the nonce values are equal, flase otherwise bool VerifyNonce(ReadOnlySpan nonceBytes); } - - /// - /// Provides INonce extensions for computing/verifying nonce values - /// - public static class NonceExtensions - { - /// - /// Computes a base32 nonce of the specified size and returns a string - /// representation - /// - /// - /// The size (in bytes) of the nonce - /// The base32 string of the computed nonce - public static string ComputeNonce(this T nonce, int size) where T: INonce - { - //Alloc bin buffer - using UnsafeMemoryHandle buffer = Memory.UnsafeAlloc(size); - //Compute nonce - nonce.ComputeNonce(buffer.Span); - //Return base32 string - return VnEncoding.ToBase32String(buffer.Span, false); - } - /// - /// Compares the base32 encoded nonce value against the previously - /// generated nonce - /// - /// - /// The base32 encoded nonce string - /// True if the nonce values are equal, flase otherwise - public static bool VerifyNonce(this T nonce, ReadOnlySpan base32Nonce) where T : INonce - { - //Alloc bin buffer - using UnsafeMemoryHandle buffer = Memory.UnsafeAlloc(base32Nonce.Length); - //Decode base32 nonce - ERRNO count = VnEncoding.TryFromBase32Chars(base32Nonce, buffer.Span); - //Verify nonce - return nonce.VerifyNonce(buffer.Span[..(int)count]); - } - } } diff --git a/lib/Plugins.Essentials/src/Accounts/ISecretProvider.cs b/lib/Plugins.Essentials/src/Accounts/ISecretProvider.cs new file mode 100644 index 0000000..41fb44d --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/ISecretProvider.cs @@ -0,0 +1,49 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: ISecretProvider.cs +* +* ISecretProvider.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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 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 VNLib.Utils; + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Provides a password hashing secret aka pepper. + /// + public interface ISecretProvider + { + /// + /// The size of the buffer to use when retrieving the secret + /// + int BufferSize { get; } + + /// + /// Writes the secret to the buffer and returns the number of bytes + /// written to the buffer + /// + /// The buffer to write the secret data to + /// The number of secret bytes written to the buffer + ERRNO GetSecret(Span buffer); + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/NonceExtensions.cs b/lib/Plugins.Essentials/src/Accounts/NonceExtensions.cs new file mode 100644 index 0000000..5a40d29 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/NonceExtensions.cs @@ -0,0 +1,75 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: NonceExtensions.cs +* +* NonceExtensions.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials 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 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 VNLib.Utils; +using VNLib.Utils.Memory; + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Provides INonce extensions for computing/verifying nonce values + /// + public static class NonceExtensions + { + /// + /// Computes a base32 nonce of the specified size and returns a string + /// representation + /// + /// + /// The size (in bytes) of the nonce + /// The base32 string of the computed nonce + public static string ComputeNonce(this T nonce, int size) where T: INonce + { + //Alloc bin buffer + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(size); + + //Compute nonce + nonce.ComputeNonce(buffer.Span); + + //Return base32 string + return VnEncoding.ToBase32String(buffer.Span, false); + } + + /// + /// Compares the base32 encoded nonce value against the previously + /// generated nonce + /// + /// + /// The base32 encoded nonce string + /// True if the nonce values are equal, flase otherwise + public static bool VerifyNonce(this T nonce, ReadOnlySpan base32Nonce) where T : INonce + { + //Alloc bin buffer + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(base32Nonce.Length); + + //Decode base32 nonce + ERRNO count = VnEncoding.TryFromBase32Chars(base32Nonce, buffer.Span); + + //Verify nonce + return nonce.VerifyNonce(buffer.Span[..(int)count]); + } + } +} diff --git a/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs b/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs index 1c3770b..9dc3ea1 100644 --- a/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs +++ b/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs @@ -32,21 +32,12 @@ using VNLib.Utils.Memory; namespace VNLib.Plugins.Essentials.Accounts { /// - /// A delegate method to recover a temporary copy of the secret/pepper - /// for a request - /// - /// The buffer to write the pepper to - /// The number of bytes written to the buffer - public delegate ERRNO SecretAction(Span buffer); - - /// - /// Provides a structrued password hashing system implementing the library + /// Provides a structured password hashing system implementing the library /// with fixed time comparison /// public sealed class PasswordHashing { - private readonly SecretAction _getter; - private readonly int _secretSize; + private readonly ISecretProvider _secret; private readonly uint TimeCost; private readonly uint MemoryCost; @@ -57,23 +48,20 @@ namespace VNLib.Plugins.Essentials.Accounts /// /// Initalizes the class /// - /// - /// The expected size of the secret (the size of the buffer to alloc for a copy) + /// The password secret provider /// A positive integer for the size of the random salt used during the hashing proccess /// The Argon2 time cost parameter /// The Argon2 memory cost parameter /// The size of the hash to produce during hashing operations /// /// The Argon2 parallelism parameter (the number of threads to use for hasing) - /// (default = 0 - the number of processors) + /// (default = 0 - defaults to the number of logical processors) /// /// - /// - public PasswordHashing(SecretAction getter, int secreteSize, int saltLen = 32, uint timeCost = 4, uint memoryCost = UInt16.MaxValue, uint parallism = 0, uint hashLen = 128) + public PasswordHashing(ISecretProvider secret, int saltLen = 32, uint timeCost = 4, uint memoryCost = UInt16.MaxValue, uint parallism = 0, uint hashLen = 128) { //Store getter - _getter = getter ?? throw new ArgumentNullException(nameof(getter)); - _secretSize = secreteSize; + _secret = secret ?? throw new ArgumentNullException(nameof(secret)); //Store parameters HashLen = hashLen; @@ -114,18 +102,18 @@ namespace VNLib.Plugins.Essentials.Accounts return false; } //alloc secret buffer - using UnsafeMemoryHandle secretBuffer = Memory.UnsafeAlloc(_secretSize, true); + using UnsafeMemoryHandle secretBuffer = MemoryUtil.UnsafeAlloc(_secret.BufferSize, true); try { //Get the secret from the callback - ERRNO count = _getter(secretBuffer.Span); + ERRNO count = _secret.GetSecret(secretBuffer.Span); //Verify return VnArgon2.Verify2id(password, passHash, secretBuffer.Span[..(int)count]); } finally { //Erase secret buffer - Memory.InitializeBlock(secretBuffer.Span); + MemoryUtil.InitializeBlock(secretBuffer.Span); } } /// @@ -140,7 +128,7 @@ namespace VNLib.Plugins.Essentials.Accounts public bool Verify(ReadOnlySpan hash, ReadOnlySpan salt, ReadOnlySpan password) { //Alloc a buffer with the same size as the hash - using UnsafeMemoryHandle hashBuf = Memory.UnsafeAlloc(hash.Length, true); + using UnsafeMemoryHandle hashBuf = MemoryUtil.UnsafeAlloc(hash.Length, true); //Hash the password with the current config Hash(password, salt, hashBuf.Span); //Compare the hashed password to the specified hash and return results @@ -164,7 +152,7 @@ namespace VNLib.Plugins.Essentials.Accounts public PrivateString Hash(ReadOnlySpan password) { //Alloc shared buffer for the salt and secret buffer - using UnsafeMemoryHandle buffer = Memory.UnsafeAlloc(SaltLen + _secretSize, true); + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(SaltLen + _secret.BufferSize, true); try { //Split buffers @@ -175,14 +163,14 @@ namespace VNLib.Plugins.Essentials.Accounts RandomHash.GetRandomBytes(saltBuf); //recover the secret - ERRNO count = _getter(secretBuf); + ERRNO count = _secret.GetSecret(secretBuf); //Hashes a password, with the current parameters return (PrivateString)VnArgon2.Hash2id(password, saltBuf, secretBuf[..(int)count], TimeCost, MemoryCost, Parallelism, HashLen); } finally { - Memory.InitializeBlock(buffer.Span); + MemoryUtil.InitializeBlock(buffer.Span); } } @@ -194,7 +182,7 @@ namespace VNLib.Plugins.Essentials.Accounts /// A of the hashed and encoded password public PrivateString Hash(ReadOnlySpan password) { - using UnsafeMemoryHandle buffer = Memory.UnsafeAlloc(SaltLen + _secretSize, true); + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(SaltLen + _secret.BufferSize, true); try { //Split buffers @@ -205,14 +193,14 @@ namespace VNLib.Plugins.Essentials.Accounts RandomHash.GetRandomBytes(saltBuf); //recover the secret - ERRNO count = _getter(secretBuf); + ERRNO count = _secret.GetSecret(secretBuf); //Hashes a password, with the current parameters return (PrivateString)VnArgon2.Hash2id(password, saltBuf, secretBuf[..(int)count], TimeCost, MemoryCost, Parallelism, HashLen); } finally { - Memory.InitializeBlock(buffer.Span); + MemoryUtil.InitializeBlock(buffer.Span); } } /// @@ -226,18 +214,18 @@ namespace VNLib.Plugins.Essentials.Accounts public void Hash(ReadOnlySpan password, ReadOnlySpan salt, Span hashOutput) { //alloc secret buffer - using UnsafeMemoryHandle secretBuffer = Memory.UnsafeAlloc(_secretSize, true); + using UnsafeMemoryHandle secretBuffer = MemoryUtil.UnsafeAlloc(_secret.BufferSize, true); try { //Get the secret from the callback - ERRNO count = _getter(secretBuffer.Span); + ERRNO count = _secret.GetSecret(secretBuffer.Span); //Hashes a password, with the current parameters VnArgon2.Hash2id(password, salt, secretBuffer.Span[..(int)count], hashOutput, TimeCost, MemoryCost, Parallelism); } finally { //Erase secret buffer - Memory.InitializeBlock(secretBuffer.Span); + MemoryUtil.InitializeBlock(secretBuffer.Span); } } } diff --git a/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs b/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs index 22cccd9..d087c06 100644 --- a/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs +++ b/lib/Plugins.Essentials/src/Extensions/JsonResponse.cs @@ -49,12 +49,19 @@ namespace VNLib.Plugins.Essentials.Extensions internal JsonResponse(IObjectRental pool) { + /* + * I am breaking the memoryhandle rules by referrencing the same + * memory handle in two different wrappers. + */ + _pool = pool; //Alloc buffer - _handle = Memory.Shared.Alloc(4096, false); + _handle = MemoryUtil.Shared.Alloc(4096, false); + //Consume handle for stream, but make sure not to dispose the stream _asStream = VnMemoryStream.ConsumeHandle(_handle, 0, false); + //Get memory owner from handle _memoryOwner = _handle.ToMemoryManager(false); } diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs index ffad607..416b004 100644 --- a/lib/Plugins.Essentials/src/HttpEntity.cs +++ b/lib/Plugins.Essentials/src/HttpEntity.cs @@ -77,6 +77,9 @@ namespace VNLib.Plugins.Essentials IsLocalConnection = entity.Server.LocalEndpoint.Address.IsLocalSubnet(TrustedRemoteIp); //Cache value IsSecure = entity.Server.IsSecure(IsBehindDownStreamServer); + + //Cache current time + RequestedTimeUtc = DateTimeOffset.UtcNow; } /// @@ -100,6 +103,11 @@ namespace VNLib.Plugins.Essentials /// or behind a trusted downstream server that is using tls. /// public readonly bool IsSecure; + /// + /// Caches a that was created when the connection was created. + /// The approximate current UTC time + /// + public readonly DateTimeOffset RequestedTimeUtc; /// /// The connection info object assocated with the entity diff --git a/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs b/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs index 13e2a84..6a974e0 100644 --- a/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs +++ b/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs @@ -106,7 +106,7 @@ namespace VNLib.Plugins.Essentials.Sessions /// public readonly Uri SpecifiedOrigin; /// - /// Privilages associated with user specified during login + /// The time the session was created /// public readonly DateTimeOffset Created; /// -- cgit