/*
* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Loading
* File: PluginSecretLoading.cs
*
* PluginSecretLoading.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.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using VNLib.Utils;
using VNLib.Utils.Memory;
using VNLib.Hashing.IdentityUtility;
namespace VNLib.Plugins.Extensions.Loading
{
///
/// Adds loading extensions for secure/centralized configuration secrets
///
public static class PluginSecretLoading
{
///
/// Gets a wrapper for the secret store for the current plugin
///
///
/// The secret store structure
public static PluginSecretStore Secrets(this PluginBase plugin) => new(plugin);
///
///
/// Gets a secret from the "secrets" element.
///
///
/// Secrets elements are merged from the host config and plugin local config 'secrets' element.
/// before searching. The plugin config takes precedence over the host config.
///
///
///
/// The name of the secret propery to get
/// The element from the configuration file with the given name, raises an exception if the secret does not exist
///
///
public static async Task GetSecretAsync(this PluginBase plugin, string secretName)
{
ISecretResult? res = await TryGetSecretAsync(plugin, secretName).ConfigureAwait(false);
return res ?? throw new KeyNotFoundException($"Missing required secret {secretName}");
}
///
///
/// Gets a secret from the "secrets" element.
///
///
/// Secrets elements are merged from the host config and plugin local config 'secrets' element.
/// before searching. The plugin config takes precedence over the host config.
///
///
///
/// The name of the secret propery to get
/// The element from the configuration file with the given name, or null if the configuration or property does not exist
///
///
public static Task TryGetSecretAsync(this PluginBase plugin, string secretName)
{
return plugin.Secrets().TryGetSecretAsync(secretName);
}
///
///
/// Gets a required secret from the "secrets" element.
///
///
/// Secrets elements are merged from the host config and plugin local config 'secrets' element.
/// before searching. The plugin config takes precedence over the host config.
///
///
///
/// The name of the secret propery to get
/// The element from the configuration file with the given name, raises an exception if the secret does not exist
///
///
public static async Task GetSecretAsync(this PluginSecretStore secrets, string secretName)
{
ISecretResult? res = await secrets.TryGetSecretAsync(secretName).ConfigureAwait(false);
return res ?? throw new KeyNotFoundException($"Missing required secret {secretName}");
}
///
/// Gets a secret at the given vault url (in the form of "vault://[mount-name]/[secret-path]?secret=[secret_name]")
///
///
/// The raw vault url to lookup
/// The string of the object at the specified vault path
///
///
///
[Obsolete("Deprecated in favor of Secrets")]
public static Task GetSecretFromVaultAsync(this PluginBase plugin, ReadOnlySpan vaultPath)
{
throw new NotSupportedException("This method is not supported in this context");
}
///
/// Gets the Secret value as a byte buffer
///
///
/// The base64 decoded secret as a byte[]
///
///
public static byte[] GetFromBase64(this ISecretResult secret)
{
ArgumentNullException.ThrowIfNull(secret);
//Temp buffer
using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(secret.Result.Length);
//Get base64
if(!Convert.TryFromBase64Chars(secret.Result, buffer.Span, out int count))
{
throw new InternalBufferTooSmallException("internal buffer too small");
}
//Copy to array
byte[] value = buffer.Span[..count].ToArray();
//Clear block before returning
MemoryUtil.InitializeBlock(buffer.Span);
return value;
}
///
/// Recovers a certificate from a PEM encoded secret
///
///
/// The parsed from the PEM encoded data
///
public static X509Certificate2 GetCertificate(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
return X509Certificate2.CreateFromPem(secret.Result);
}
///
/// Gets the secret value as a secret result
///
///
/// The document parsed from the secret value
public static JsonDocument GetJsonDocument(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
//Alloc buffer, utf8 so 1 byte per char
using IMemoryHandle buffer = MemoryUtil.SafeAlloc(secret.Result.Length);
//Get utf8 bytes
int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span);
//Reader and parse
Utf8JsonReader reader = new(buffer.Span[..count]);
return JsonDocument.ParseValue(ref reader);
}
///
/// Gets a SPKI encoded public key from a secret
///
///
/// The parsed from the SPKI public key
///
public static PublicKey GetPublicKey(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
//Alloc buffer, base64 is larger than binary value so char len is large enough
using IMemoryHandle buffer = MemoryUtil.SafeAlloc(secret.Result.Length);
//Get base64 bytes
ERRNO count = VnEncoding.TryFromBase64Chars(secret.Result, buffer.Span);
//Parse the SPKI from base64
return PublicKey.CreateFromSubjectPublicKeyInfo(buffer.Span[..(int)count], out _);
}
///
/// Gets the value of the as a
/// container
///
///
/// The from the secret value
///
///
public static PrivateKey GetPrivateKey(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
return new PrivateKey(secret);
}
///
/// Gets a from a secret value
///
///
/// The from the result
///
///
///
public static ReadOnlyJsonWebKey GetJsonWebKey(this ISecretResult secret)
{
_ = secret ?? throw new ArgumentNullException(nameof(secret));
//Alloc buffer, utf8 so 1 byte per char
using IMemoryHandle buffer = MemoryUtil.SafeAlloc(secret.Result.Length);
//Get utf8 bytes
int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span);
return new ReadOnlyJsonWebKey(buffer.Span[..count]);
}
#nullable disable
///
/// Converts the secret recovery task to return the base64 decoded secret as a byte[]
///
///
/// A task whos result the base64 decoded secret as a byte[]
///
///
public static async Task ToBase64Bytes(this Task secret)
{
ArgumentNullException.ThrowIfNull(secret);
using ISecretResult sec = await secret.ConfigureAwait(false);
return sec?.GetFromBase64();
}
///
/// Gets a task that resolves a
/// from a task
///
///
/// The from the secret, or null if the secret was not found
///
public static async Task ToJsonWebKey(this Task secret)
{
ArgumentNullException.ThrowIfNull(secret);
using ISecretResult sec = await secret.ConfigureAwait(false);
return sec?.GetJsonWebKey();
}
///
/// Gets a task that resolves a
/// from a task
///
///
///
/// A value that inidcates that a value is required from the result,
/// or a is raised
///
/// The from the secret, or throws if the key was not found
///
///
public static async Task ToJsonWebKey(this Task secret, bool required)
{
ArgumentNullException.ThrowIfNull(secret);
using ISecretResult sec = await secret.ConfigureAwait(false);
//If required is true and result is null, raise an exception
return required && sec == null ? throw new KeyNotFoundException("A required secret was missing") : (sec?.GetJsonWebKey()!);
}
///
/// Converts a async operation to a lazy result that can be awaited, that transforms the result
/// to your desired type. If the result is null, the default value of is returned
///
///
///
/// Your function to transform the secret to its output form
/// A
///
public static IAsyncLazy ToLazy(this Task result, Func transformer)
{
ArgumentNullException.ThrowIfNull(result);
ArgumentNullException.ThrowIfNull(transformer);
//standard secret transformer
static async Task Run(Task tr, Func transformer)
{
using ISecretResult res = await tr.ConfigureAwait(false);
return res == null ? default : transformer(res);
}
return Run(result, transformer).AsLazy();
}
///
/// Converts a async operation to a lazy result that can be awaited, that transforms the result
/// to your desired type. If the result is null, the default value of is returned
///
///
///
/// Your function to transform the secret to its output form
/// A
///
public static IAsyncLazy ToLazy(this Task result, Func> transformer)
{
ArgumentNullException.ThrowIfNull(result);
ArgumentNullException.ThrowIfNull(transformer);
//Transform with task transformer
static async Task Run(Task tr, Func> transformer)
{
using ISecretResult res = await tr.ConfigureAwait(false);
return res == null ? default : await transformer(res).ConfigureAwait(false);
}
return Run(result, transformer).AsLazy();
}
#nullable enable
}
}