/* * 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) { ArgumentNullException.ThrowIfNull(secret); return new PrivateKey(secret); } /// /// Gets a from a secret value /// /// /// The from the result /// /// /// public static ReadOnlyJsonWebKey GetJsonWebKey(this ISecretResult secret) { ArgumentNullException.ThrowIfNull(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 ReadOnlyJsonWebKey.FromUtf8Bytes(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 } }