/*
* 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;
}
}
}
}
}