From e77477b81e5623502b19db0fb29d4ea88c26b934 Mon Sep 17 00:00:00 2001 From: vnugent Date: Sat, 8 Apr 2023 16:43:52 -0400 Subject: Passwords singlton and user-loading --- .../src/ConfigrationValidationException.cs | 42 ---- .../src/ConfigurationExtensions.cs | 18 +- .../src/ConfigurationValidationException.cs | 42 ++++ .../src/LoadingExtensions.cs | 111 +---------- .../src/ManagedPasswordHashing.cs | 211 +++++++++++++++++++++ .../src/VaultSecrets.cs | 7 + 6 files changed, 271 insertions(+), 160 deletions(-) delete mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/ConfigrationValidationException.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationValidationException.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs (limited to 'lib/VNLib.Plugins.Extensions.Loading/src') diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigrationValidationException.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigrationValidationException.cs deleted file mode 100644 index 83ce558..0000000 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigrationValidationException.cs +++ /dev/null @@ -1,42 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: ConfigrationValidationException.cs -* -* ConfigrationValidationException.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading 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.Extensions.Loading 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; - -namespace VNLib.Plugins.Extensions.Loading -{ - /// - /// An exception raised when a configuration validation exception has occured - /// - public class ConfigrationValidationException : Exception - { - public ConfigrationValidationException(string message) : base(message) - {} - - public ConfigrationValidationException(string message, Exception innerException) : base(message, innerException) - {} - public ConfigrationValidationException() - {} - } -} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs index 0ae6ed6..190c153 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs @@ -221,12 +221,12 @@ namespace VNLib.Plugins.Extensions.Loading /// /// Deserialzes the configuration to the desired object and calls its /// method. Validation exceptions - /// are wrapped in a + /// are wrapped in a /// /// /// /// - /// + /// public static T DeserialzeAndValidate(this IConfigScope scope) where T : IOnConfigValidation { T conf = scope.Deserialze(); @@ -236,7 +236,7 @@ namespace VNLib.Plugins.Extensions.Loading } catch(Exception ex) { - throw new ConfigrationValidationException($"Configuration validation failed for type {typeof(T).Name}", ex); + throw new ConfigurationValidationException($"Configuration validation failed for type {typeof(T).Name}", ex); } return conf; } @@ -261,7 +261,7 @@ namespace VNLib.Plugins.Extensions.Loading /// and deserializes it into the desired type. /// /// If the type inherits the - /// method is invoked, and exceptions are warpped in + /// method is invoked, and exceptions are warpped in /// /// /// If the type inherits the @@ -271,7 +271,7 @@ namespace VNLib.Plugins.Extensions.Loading /// The configuration type /// /// The deserialzed configuration element - /// + /// public static TConfig GetConfigElement(this PluginBase plugin) { //Deserialze the element @@ -286,7 +286,7 @@ namespace VNLib.Plugins.Extensions.Loading } catch (Exception ex) { - throw new ConfigrationValidationException($"Configuration validation failed for type {typeof(TConfig).Name}", ex); + throw new ConfigurationValidationException($"Configuration validation failed for type {typeof(TConfig).Name}", ex); } } @@ -304,7 +304,7 @@ namespace VNLib.Plugins.Extensions.Loading /// and deserializes it into the desired type. /// /// If the type inherits the - /// method is invoked, and exceptions are warpped in + /// method is invoked, and exceptions are warpped in /// /// /// If the type inherits the @@ -315,7 +315,7 @@ namespace VNLib.Plugins.Extensions.Loading /// /// The configuration element name override /// The deserialzed configuration element - /// + /// public static TConfig GetConfigElement(this PluginBase plugin, string elementName) { //Deserialze the element @@ -330,7 +330,7 @@ namespace VNLib.Plugins.Extensions.Loading } catch (Exception ex) { - throw new ConfigrationValidationException($"Configuration validation failed for type {typeof(TConfig).Name}", ex); + throw new ConfigurationValidationException($"Configuration validation failed for type {typeof(TConfig).Name}", ex); } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationValidationException.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationValidationException.cs new file mode 100644 index 0000000..ebf4d9e --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationValidationException.cs @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ConfigurationValidationException.cs +* +* ConfigurationValidationException.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading 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.Extensions.Loading 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; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// + /// An exception raised when a configuration validation exception has occured + /// + public class ConfigurationValidationException : Exception + { + public ConfigurationValidationException(string message) : base(message) + {} + + public ConfigurationValidationException(string message, Exception innerException) : base(message, innerException) + {} + public ConfigurationValidationException() + {} + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs index 3e151b4..5511398 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs @@ -32,14 +32,11 @@ using System.Threading.Tasks; using System.Collections.Generic; using System.Runtime.CompilerServices; -using VNLib.Utils; -using VNLib.Utils.Memory; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; -using VNLib.Plugins.Essentials.Accounts; namespace VNLib.Plugins.Extensions.Loading -{ +{ /// /// Provides common loading (and unloading when required) extensions for plugins @@ -54,6 +51,7 @@ namespace VNLib.Plugins.Extensions.Loading public const string DEBUG_CONFIG_KEY = "debug"; public const string SECRETS_CONFIG_KEY = "secrets"; public const string PASSWORD_HASHING_KEY = "passwords"; + public const string CUSTOM_PASSWORD_ASM_KEY = "custom_asm"; /* * Plugin local cache used for storing singletons for a plugin instance @@ -95,23 +93,6 @@ namespace VNLib.Plugins.Extensions.Loading public static T GetOrCreateSingleton(PluginBase plugin, Func serviceFactory) => (T)GetOrCreateSingleton(plugin, typeof(T), p => serviceFactory(p)!); - - /// - /// 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 IPasswordHashingProvider GetPasswords(this PluginBase plugin) - { - plugin.ThrowIfUnloaded(); - //Check if a password configuration element is loaded, otherwise load with defaults - return plugin.GetOrCreateSingleton().Passwords; - } - /// /// Loads an assembly into the current plugin's load context and will unload when disposed /// or the plugin is unloaded from the host application. @@ -551,93 +532,5 @@ namespace VNLib.Plugins.Extensions.Loading return lazyFactory; } } - - [ConfigurationName(PASSWORD_HASHING_KEY, Required = false)] - private sealed class SecretProvider : VnDisposeable, ISecretProvider, IAsyncConfigurable - { - private byte[]? _pepper; - private Exception? _error; - - public SecretProvider(PluginBase plugin, IConfigScope config) - { - if(config.TryGetValue("args", out JsonElement el)) - { - //Convert to dict - IReadOnlyDictionary hashingArgs = el.EnumerateObject().ToDictionary(static k => k.Name, static v => v.Value); - - //Get hashing arguments - uint saltLen = hashingArgs["salt_len"].GetUInt32(); - uint hashLen = hashingArgs["hash_len"].GetUInt32(); - uint timeCost = hashingArgs["time_cost"].GetUInt32(); - uint memoryCost = hashingArgs["memory_cost"].GetUInt32(); - uint parallelism = hashingArgs["parallelism"].GetUInt32(); - //Load passwords - Passwords = new(this, (int)saltLen, timeCost, memoryCost, parallelism, hashLen); - } - else - { - Passwords = new(this); - } - } - - public SecretProvider(PluginBase plugin) - { - Passwords = new(this); - } - - - public PasswordHashing Passwords { get; } - - /// - public int BufferSize - { - get - { - Check(); - return _pepper!.Length; - } - } - - public ERRNO GetSecret(Span buffer) - { - Check(); - //Coppy pepper to buffer - _pepper.CopyTo(buffer); - //Return pepper length - return _pepper!.Length; - } - - protected override void Check() - { - base.Check(); - if(_error != null) - { - throw _error; - } - } - - protected override void Free() - { - //Clear the pepper if set - MemoryUtil.InitializeBlock(_pepper.AsSpan()); - } - - public async Task ConfigureServiceAsync(PluginBase plugin) - { - try - { - //Get the pepper from secret storage - _pepper = await plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY).ToBase64Bytes(); - } - catch (Exception ex) - { - //Store exception for re-propagation - _error = ex; - - //Propagate exception to system - throw; - } - } - } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs new file mode 100644 index 0000000..522bfae --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ManagedPasswordHashing.cs @@ -0,0 +1,211 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ManagedPasswordHashing.cs +* +* ManagedPasswordHashing.cs is part of VNLib.Plugins.Extensions.Loading which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading 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.Extensions.Loading 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.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; +using VNLib.Plugins.Essentials.Accounts; + +namespace VNLib.Plugins.Extensions.Loading +{ + + /// + /// A plugin configurable managed implementation. Users may load custom + /// assemblies backing instances of this class or configure the implementation + /// + [ConfigurationName(LoadingExtensions.PASSWORD_HASHING_KEY, Required = false)] + public sealed class ManagedPasswordHashing : IPasswordHashingProvider + { + public ManagedPasswordHashing(PluginBase plugin, IConfigScope config) + { + //Check for custom hashing assembly + if (config.TryGetValue(LoadingExtensions.CUSTOM_PASSWORD_ASM_KEY, out JsonElement el)) + { + string customAsm = el.GetString() ?? throw new KeyNotFoundException("You must specify a string file path for your custom password hashing assembly"); + + //Load the custom assembly + AssemblyLoader prov = plugin.LoadAssembly(customAsm); + + //Configure async + if (prov.Resource is IAsyncConfigurable ac) + { + //Configure async + _ = plugin.ConfigureServiceAsync(ac); + } + + //Store + Passwords = new CustomPasswordHashingAsm(prov); + } + else + { + Passwords = plugin.GetOrCreateSingleton().Passwords; + } + } + + public ManagedPasswordHashing(PluginBase plugin) + { + //Only configure a default password impl + Passwords = plugin.GetOrCreateSingleton().Passwords; + } + + /// + /// The underlying + /// + public IPasswordHashingProvider Passwords { get; } + + /// + public bool Verify(ReadOnlySpan passHash, ReadOnlySpan password) => Passwords.Verify(passHash, password); + + /// + public bool Verify(ReadOnlySpan passHash, ReadOnlySpan password) => Passwords.Verify(passHash, password); + + /// + public PrivateString Hash(ReadOnlySpan password) => Passwords.Hash(password); + + /// + public PrivateString Hash(ReadOnlySpan password) => Passwords.Hash(password); + + /// + public ERRNO Hash(ReadOnlySpan password, Span hashOutput) => Passwords.Hash(password, hashOutput); + + sealed class CustomPasswordHashingAsm : IPasswordHashingProvider + { + private readonly AssemblyLoader _loader; + + public CustomPasswordHashingAsm(AssemblyLoader loader) + { + _loader = loader; + } + + /* + * Password hashing isnt a super high performance system + * so adding method overhead shouldnt be a large issue for the + * asm wrapper providing unload protection + */ + + public PrivateString Hash(ReadOnlySpan password) => _loader.Resource.Hash(password); + + public PrivateString Hash(ReadOnlySpan password) => _loader.Resource.Hash(password); + + public ERRNO Hash(ReadOnlySpan password, Span hashOutput) => _loader.Resource.Hash(password, hashOutput); + + public bool Verify(ReadOnlySpan passHash, ReadOnlySpan password) => _loader.Resource.Verify(passHash, password); + + public bool Verify(ReadOnlySpan passHash, ReadOnlySpan password) => _loader.Resource.Verify(passHash, password); + } + + private sealed class SecretProvider : VnDisposeable, ISecretProvider, IAsyncConfigurable + { + private byte[]? _pepper; + private Exception? _error; + + public SecretProvider(PluginBase plugin, IConfigScope config) + { + if (config.TryGetValue("args", out JsonElement el)) + { + //Convert to dict + IReadOnlyDictionary hashingArgs = el.EnumerateObject().ToDictionary(static k => k.Name, static v => v.Value); + + //Get hashing arguments + uint saltLen = hashingArgs["salt_len"].GetUInt32(); + uint hashLen = hashingArgs["hash_len"].GetUInt32(); + uint timeCost = hashingArgs["time_cost"].GetUInt32(); + uint memoryCost = hashingArgs["memory_cost"].GetUInt32(); + uint parallelism = hashingArgs["parallelism"].GetUInt32(); + //Load passwords + Passwords = new(this, (int)saltLen, timeCost, memoryCost, parallelism, hashLen); + } + else + { + Passwords = new(this); + } + } + + public SecretProvider(PluginBase plugin) + { + Passwords = new(this); + } + + + public PasswordHashing Passwords { get; } + + /// + public int BufferSize + { + get + { + Check(); + return _pepper!.Length; + } + } + + public ERRNO GetSecret(Span buffer) + { + Check(); + //Coppy pepper to buffer + _pepper.CopyTo(buffer); + //Return pepper length + return _pepper!.Length; + } + + protected override void Check() + { + base.Check(); + if (_error != null) + { + throw _error; + } + } + + protected override void Free() + { + //Clear the pepper if set + MemoryUtil.InitializeBlock(_pepper.AsSpan()); + } + + public async Task ConfigureServiceAsync(PluginBase plugin) + { + try + { + //Get the pepper from secret storage + _pepper = await plugin.TryGetSecretAsync(LoadingExtensions.PASSWORD_HASHING_KEY).ToBase64Bytes(); + } + catch (Exception ex) + { + //Store exception for re-propagation + _error = ex; + + //Propagate exception to system + throw; + } + } + } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs b/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs index 4cf1c9d..9e3c222 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs @@ -56,6 +56,7 @@ namespace VNLib.Plugins.Extensions.Loading public const string VAULT_TOKEN_KEY = "token"; public const string VAULT_ROLE_KEY = "role"; public const string VAULT_SECRET_KEY = "secret"; + public const string VAULT_TOKNE_ENV_NAME = "VNLIB_PLUGINS_VAULT_TOKEN"; public const string VAULT_URL_KEY = "url"; @@ -306,6 +307,12 @@ namespace VNLib.Plugins.Extensions.Loading { authMethod = new AppRoleAuthMethodInfo(roleEl.GetString(), secretEl.GetString()); } + //Try to get the token as an environment variable + else if(Environment.GetEnvironmentVariable(VAULT_TOKNE_ENV_NAME) != null) + { + string tokenValue = Environment.GetEnvironmentVariable(VAULT_TOKNE_ENV_NAME)!; + authMethod = new TokenAuthMethodInfo(tokenValue); + } else { throw new KeyNotFoundException($"Failed to load the vault authentication method from {VAULT_OBJECT_NAME}"); -- cgit