2025-01-07 09:40:39 +01:00
// Copyright (C) 2025, The Duplicati Team
2024-10-28 08:08:29 +01:00
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
2024-10-24 15:56:39 +02:00
#nullable enable
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Threading.Tasks ;
2024-11-04 15:57:56 +01:00
using System.Web ;
2024-10-24 15:56:39 +02:00
using Duplicati.Library.DynamicLoader ;
using Duplicati.Library.Interface ;
using Duplicati.Library.Logging ;
using Duplicati.Library.Utility ;
namespace Duplicati.Library.Main ;
/// <summary>
/// Helper to apply secret provider to arguments
/// </summary>
public static class SecretProviderHelper
{
/// <summary>
/// The log tag
/// </summary>
private static readonly string LOGTAG = Log . LogTagFromType < SecretProviderLoader >();
2024-10-26 08:37:26 +02:00
/// <summary>
/// The default pattern to use for matching
/// </summary>
public const string DEFAULT_PATTERN = "$" ;
2024-10-24 15:56:39 +02:00
/// <summary>
/// The different levels of caching permitted for the secrets
/// </summary>
public enum CachingLevel
{
/// <summary>
/// Values are always fetched from the provider
/// </summary>
None ,
/// <summary>
/// Values are cached in memory and used if the provider is not available
/// </summary>
InMemory ,
/// <summary>
/// Values are cached in memory and saved to disk with encryption.
/// If the provider is not available, the values are fetched from disk.
/// </summary>
Persistent
}
/// <summary>
/// Creates an instance of a secret provider with caching enabled
/// </summary>
/// <param name="config">The configuration string</param>
/// <param name="cachingLevel">The caching level</param>
/// <param name="persistedFolder">The folder to persist the cache to</param>
/// <param name="salt">The salt to use for hashing</param>
2024-10-26 08:37:26 +02:00
/// <param name="pattern">The pattern to use for matching</param>
2024-10-24 15:56:39 +02:00
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The secret provider instance</returns>
2024-10-26 08:37:26 +02:00
public static async Task < ISecretProvider > CreateInstanceAsync ( string config , CachingLevel cachingLevel , string persistedFolder , string salt , string pattern , CancellationToken cancelToken )
2024-10-24 15:56:39 +02:00
{
2024-10-24 16:52:09 +02:00
var provider = SecretProviderLoader . CreateInstance ( config );
2024-10-26 08:37:26 +02:00
var sp = WrapWithCache ( config , provider , cachingLevel , persistedFolder , salt , pattern );
2024-10-24 15:56:39 +02:00
await sp . InitializeAsync ( new System . Uri ( config ), cancelToken ). ConfigureAwait ( false );
return sp ;
}
2024-10-25 23:13:38 +02:00
/// <summary>
/// Wraps a secret provider with caching
/// </summary>
/// <param name="config">The configuration string</param>
/// <param name="provider">The provider to wrap</param>
/// <param name="cachingLevel">The caching level</param>
/// <param name="persistedFolder">The folder to persist the cache to</param>
/// <param name="salt">The salt to use for hashing</param>
2024-10-26 08:37:26 +02:00
/// <param name="pattern">The pattern to use for matching</param>
2024-10-25 23:13:38 +02:00
/// <returns>The wrapped secret provider</returns>
2024-10-26 08:37:26 +02:00
public static ISecretProvider WrapWithCache ( string config , ISecretProvider provider , CachingLevel cachingLevel , string persistedFolder , string salt , string? pattern )
=> new SecretProviderCached ( config , provider , cachingLevel , persistedFolder , salt , pattern );
2024-10-25 23:13:38 +02:00
2025-12-07 22:26:37 +01:00
/// <summary>
/// Gets the default secret provider, if any
/// </summary>
/// <param name="options">The options passed</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns></returns>
public static async Task < ISecretProvider ?> GetDefaultSecretProvider ( Dictionary < string , string? > options , CancellationToken cancellationToken )
{
var providerConfig = options . GetValueOrDefault ( "secret-provider" );
if (! string . IsNullOrWhiteSpace ( providerConfig ))
{
var provider = SecretProviderLoader . CreateInstance ( providerConfig );
if ( provider ?. IsSetSupported == true )
return provider ;
}
return await SecretProviderLoader . GetDefaultSecretProviderForOperatingSystem ( cancellationToken ). ConfigureAwait ( false );
}
2024-10-24 15:56:39 +02:00
/// <summary>
/// Applies the secret provider to the arguments.
/// Note that this method modifes the arguments and options in place.
/// </summary>
2024-11-04 15:57:56 +01:00
/// <param name="realUriArguments">The arguments to modify, of type <see cref="System.Uri"/></param>
/// <param name="internalUriArguments">The arguments to modify, of type <see cref="Library.Utility.Uri"/></param>
2024-10-24 15:56:39 +02:00
/// <param name="options">The options to modify</param>
/// <param name="persistedFolder">The persisted secret cache folder</param>
/// <param name="fallbackProvider">The fallback provider to use if no provider is specified</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>The secret provider</returns>
2024-11-04 15:57:56 +01:00
public static async Task < ISecretProvider ?> ApplySecretProviderAsync ( System . Uri ?[] realUriArguments , Library . Utility . Uri [] internalUriArguments , Dictionary < string , string? > options , string persistedFolder , ISecretProvider ? fallbackProvider , CancellationToken cancellationToken )
2024-10-24 15:56:39 +02:00
{
var provider = options . GetValueOrDefault ( "secret-provider" );
if ( string . IsNullOrWhiteSpace ( provider ) && fallbackProvider == null )
return null ;
2024-10-26 08:37:26 +02:00
var pattern = options . GetValueOrDefault ( "secret-provider-pattern" );
2024-10-24 15:56:39 +02:00
ISecretProvider secretProvider ;
if ( string . IsNullOrWhiteSpace ( provider ))
{
secretProvider = fallbackProvider
?? throw new InvalidOperationException ( "No secret provider specified" );
}
else
{
2024-10-24 16:52:09 +02:00
var newProvider = SecretProviderLoader . CreateInstance ( provider );
2024-10-24 15:56:39 +02:00
2024-10-25 23:13:38 +02:00
// Weak salt, but semi-static
2024-10-24 15:56:39 +02:00
string salt ;
using ( var hasher = HashFactory . CreateHasher ( HashFactory . SHA256 ))
salt = Environment . MachineName . ComputeHashToHex ( hasher );
2024-10-25 23:13:38 +02:00
var cachingLevel = Library . Utility . Utility . ParseEnumOption ( options , "secret-provider-cache" , CachingLevel . None );
2024-10-26 08:37:26 +02:00
secretProvider = WrapWithCache ( provider , newProvider , cachingLevel , persistedFolder , salt , pattern );
2024-10-24 15:56:39 +02:00
await secretProvider . InitializeAsync ( new System . Uri ( provider ), cancellationToken ). ConfigureAwait ( false );
}
2024-10-26 08:37:26 +02:00
if ( string . IsNullOrWhiteSpace ( pattern ) && secretProvider is SecretProviderCached cached )
pattern = cached . Pattern ;
2024-10-24 15:56:39 +02:00
if ( string . IsNullOrWhiteSpace ( pattern ))
2024-11-01 08:47:36 +01:00
pattern = DEFAULT_PATTERN ;
2024-10-24 15:56:39 +02:00
2024-11-04 15:57:56 +01:00
await ReplaceSecretsAsync ( secretProvider , realUriArguments , internalUriArguments , options , pattern , cancellationToken ). ConfigureAwait ( false );
2024-10-24 15:56:39 +02:00
return secretProvider ;
}
/// <summary>
/// Helper method that finds all secrets matching the prefix and replaces them with the resolved values
/// </summary>
/// <param name="provider">The secret provider to use</param>
2024-11-04 15:57:56 +01:00
/// <param name="realUriArguments">The arguments to modify, of type <see cref="System.Uri"/></param>
/// <param name="internalUriArguments">The arguments to modify, of type <see cref="Library.Utility.Uri"/></param>
2024-10-24 15:56:39 +02:00
/// <param name="options">Any options to update</param>
/// <param name="matchpattern">The prefix to look for</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
2024-11-04 15:57:56 +01:00
public static async Task ReplaceSecretsAsync ( ISecretProvider provider , System . Uri ?[] realUriArguments , Library . Utility . Uri [] internalUriArguments , Dictionary < string , string? > options , string matchpattern , CancellationToken cancelToken )
2024-10-24 15:56:39 +02:00
{
// Unwrap ${} to support ${name is long}
var suffix = string . Empty ;
2025-06-10 09:40:35 +02:00
var matcher = @"(\w|_|-|/)" ;
2024-10-24 15:56:39 +02:00
if ( matchpattern . EndsWith ( "{}" ) || matchpattern . EndsWith ( "()" ) || matchpattern . EndsWith ( "[]" ))
{
suffix = matchpattern [^ 1. .];
matchpattern = matchpattern [..^ 1 ];
matcher = @"[^" + Regex . Escape ( suffix ) + "]" ;
}
// For the values, they could be urls, so we need to look inside the strings
var pattern = new Regex ( @ $"{Regex.Escape(matchpattern)}(?<key>{matcher}+){Regex.Escape(suffix)}" , RegexOptions . ExplicitCapture );
2025-06-10 09:40:35 +02:00
// Collect all partial matches, so we can report them later
var partialMatches = new HashSet < string >( StringComparer . OrdinalIgnoreCase );
2024-10-24 15:56:39 +02:00
// When we get the secrets, replace these values
var optionsMap = options
. Where ( x => ! x . Key . StartsWith ( "secret-provider" , StringComparison . OrdinalIgnoreCase ))
2025-06-10 09:40:35 +02:00
. Select ( x => ( x . Key , Secret : GetKeyEx ( x . Value , pattern , matchpattern , partialMatches )))
2024-10-24 15:56:39 +02:00
. Where ( x => ! string . IsNullOrWhiteSpace ( x . Secret ))
2024-11-04 15:57:56 +01:00
. GroupBy ( x => x . Secret !)
2024-10-24 15:56:39 +02:00
. ToDictionary ( x => x . Key , x => x . Select ( y => y . Key ). ToArray ());
2024-11-04 15:57:56 +01:00
var realUriMap = realUriArguments
. Zip ( Enumerable . Range ( 0 , realUriArguments . Length ))
. Where ( x => ! string . IsNullOrWhiteSpace ( x . First ?. Query ))
. Select ( x => ( Source : x . Second , Params : HttpUtility . ParseQueryString ( x . First !. Query )))
. SelectMany ( x => x . Params . AllKeys . Select ( k => ( Source : x . Source , Key : k , Value : x . Params [ k ])))
2025-06-10 09:40:35 +02:00
. Select ( x => ( x . Source , x . Key , Secret : GetKeyEx ( x . Value , pattern , matchpattern , partialMatches )))
2024-11-04 15:57:56 +01:00
. Where ( x => ! string . IsNullOrWhiteSpace ( x . Secret ))
. GroupBy ( x => x . Secret !)
. ToDictionary ( x => x . Key , x => x . Select ( y => ( y . Source , y . Key )). ToArray ());
var internalUriMap = internalUriArguments
. Zip ( Enumerable . Range ( 0 , internalUriArguments . Length ))
. Select ( x => ( Source : x . Second , Value : x . First ))
. Where ( x => ! string . IsNullOrWhiteSpace ( x . Value . Query ))
. Select ( x => ( Source : x . Source , Params : x . Value . QueryParameters ))
. SelectMany ( x => x . Params . AllKeys . Select ( k => ( Source : x . Source , Key : k , Value : x . Params [ k ])))
2025-06-10 09:40:35 +02:00
. Select ( x => ( x . Source , x . Key , Secret : GetKeyEx ( x . Value , pattern , matchpattern , partialMatches )))
2024-11-04 15:57:56 +01:00
. Where ( x => ! string . IsNullOrWhiteSpace ( x . Secret ))
. GroupBy ( x => x . Secret !)
. ToDictionary ( x => x . Key , x => x . Select ( y => ( y . Source , y . Key )). ToArray ());
var secrets = realUriMap . Keys
. Concat ( internalUriMap . Keys )
2024-10-24 15:56:39 +02:00
. Concat ( optionsMap . Keys )
. Distinct ()
. ToArray ();
2025-06-10 09:40:35 +02:00
// If there are partial matches, log them
if ( partialMatches . Count > 0 )
Log . WriteWarningMessage ( LOGTAG , "SecretProviderPartialMatches" , null , "Found {0} partial secret matches where the value starts with the pattern {1}, value transformation not applied to those" , partialMatches . Count , matchpattern );
2024-10-24 15:56:39 +02:00
if ( secrets . Length == 0 )
return ;
var translated = await provider . ResolveSecretsAsync ( secrets , cancelToken ). ConfigureAwait ( false );
2024-10-25 23:13:38 +02:00
// Sanity check the results to guard against faulty providers
if ( translated . Any ( x => string . IsNullOrWhiteSpace ( x . Value )))
throw new InvalidOperationException ( "The secret provider returned an empty key" );
2025-05-16 15:53:59 +02:00
// Sanity check the results to guard against faulty providers
if ( secrets . Any ( x => ! translated . ContainsKey ( x )))
throw new InvalidOperationException ( "The secret provider did not return all values" );
2024-10-24 15:56:39 +02:00
// Update options by replacing values
foreach ( var v in optionsMap )
foreach ( var k in v . Value )
options [ k ] = translated [ v . Key ];
2024-11-04 15:57:56 +01:00
// Update real uri arguments by replacing values
foreach ( var v in realUriMap )
foreach ( var ( s , k ) in v . Value )
{
var builder = new UriBuilder ( realUriArguments [ s ]!);
var query = HttpUtility . ParseQueryString ( builder . Query );
query [ k ] = translated [ v . Key ];
builder . Query = query . ToString ();
realUriArguments [ s ] = builder . Uri ;
}
// Update internal uri arguments by replacing values
foreach ( var v in internalUriMap )
foreach ( var ( s , k ) in v . Value )
{
var uri = internalUriArguments [ s ];
var kp = uri . QueryParameters ;
2024-11-05 12:44:00 +01:00
kp [ k ] = Library . Utility . Uri . UrlEncode ( translated [ v . Key ]);
2024-11-04 15:57:56 +01:00
uri = uri . SetQuery ( Library . Utility . Uri . BuildUriQuery ( kp ));
internalUriArguments [ s ] = uri ;
}
2024-10-24 15:56:39 +02:00
return ;
}
2025-12-07 22:26:37 +01:00
/// <summary>
/// Resolves a single secret from the provider
/// </summary>
/// <param name="provider">The secret provider</param>
/// <param name="name">The name of the secret</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The resolved secret</returns>
public static async Task < string > ResolveSecretAsync ( this ISecretProvider provider , string name , CancellationToken cancelToken )
=> ( await provider . ResolveSecretsAsync ([ name ], cancelToken ). ConfigureAwait ( false ))[ name ];
2025-06-10 09:40:35 +02:00
/// <summary>
/// Gets the key from a value using the pattern, and also collects partial matches
/// </summary>
/// <param name="value">The value to get the key from</param>
/// <param name="pattern">The pattern to use</param>
/// <param name="prefix">The prefix to look for</param>
/// <param name="partialMatches">A set to collect partial matches</param>
/// <returns>>The key or null if not found</returns>
private static string? GetKeyEx ( string? value , Regex pattern , string prefix , HashSet < string > partialMatches )
{
if ( string . IsNullOrWhiteSpace ( value ))
return null ;
var match = GetKey ( value , pattern );
if ( match != null )
return match ;
// If the value starts with the prefix, but does not match the pattern, it is a partial match
if ( value . StartsWith ( prefix , StringComparison . OrdinalIgnoreCase ))
partialMatches . Add ( value );
return null ;
}
2024-11-04 15:57:56 +01:00
/// <summary>
/// Gets the key from a value using the pattern
/// </summary>
/// <param name="value">The value to get the key from</param>
/// <param name="pattern">The pattern to use</param>
/// <returns>The key or null if not found</returns>
private static string? GetKey ( string? value , Regex pattern )
{
if ( string . IsNullOrWhiteSpace ( value ))
return null ;
var match = pattern . Match ( value );
if (! match . Success || string . IsNullOrWhiteSpace ( match . Groups [ "key" ]. Value ) || match . Length != value . Length )
return null ;
return match . Groups [ "key" ]. Value ;
}
2024-10-24 15:56:39 +02:00
/// <summary>
/// A cache for secret provider values
/// </summary>
private class SecretProviderCached : ISecretProvider
{
/// <summary>
/// The provider being cached
/// </summary>
private readonly ISecretProvider _provider ;
/// <summary>
/// A flag indicating if the provider has been initialized
/// </summary>
private bool _initialized ;
/// <summary>
/// The caching level
/// </summary>
private readonly CachingLevel _cachingLevel ;
/// <summary>
/// The configuration string
/// </summary>
private readonly string _config ;
/// <summary>
/// The salt used for hashing and uniqueness
/// </summary>
private readonly string _salt ;
/// <summary>
/// The persisted file; null if not persistent
/// </summary>
private readonly string? _persistedFile ;
/// <summary>
/// The passphrase used to encrypt the persisted file
/// </summary>
private readonly string? _passphrase ;
/// <summary>
/// The lock object guarding _cache
/// </summary>
private static readonly object _lock = new ();
/// <summary>
/// The in-memory cache of secrets
/// </summary>
private static readonly Dictionary < string , Dictionary < string , string >> _cache = new ();
2024-10-26 08:37:26 +02:00
/// <summary>
/// The pattern associated with the provider
/// </summary>
public string Pattern { get ; }
2024-10-24 15:56:39 +02:00
/// <summary>
/// Creates a new instance of the secret provider cache
/// </summary>
/// <param name="config">The configuration string</param>
/// <param name="provider">The provider to cache</param>
/// <param name="cachingLevel">The caching level</param>
/// <param name="persistedFolder">The folder to persist the cache to</param>
/// <param name="salt">The salt to use for hashing</param>
2024-10-26 08:37:26 +02:00
/// <param name="pattern">The pattern to use for matching</param>
public SecretProviderCached ( string config , ISecretProvider provider , CachingLevel cachingLevel , string persistedFolder , string salt , string? pattern )
2024-10-24 15:56:39 +02:00
{
_provider = provider ;
_cachingLevel = cachingLevel ;
_config = config ;
_salt = salt ;
2024-10-26 08:37:26 +02:00
Pattern = pattern ?? DEFAULT_PATTERN ;
2024-10-24 15:56:39 +02:00
if ( cachingLevel == CachingLevel . Persistent )
{
// Create a unique file name for the cache, tied to the configuration
var name = Convert . ToBase64String ( Library . Utility . Utility . RepeatedHashWithSalt ( config , salt ))[.. 12 ];
_persistedFile = Path . Combine ( persistedFolder , $"secret-cache-{name}.json.aes" );
// If either the salt of the config changes, we loose the cache, both the filename and password will fail
using ( var hasher = HashFactory . CreateHasher ( HashFactory . SHA256 ))
_passphrase = Convert . ToBase64String ( $"{_salt}:{_config}" . ComputeHash ( hasher ));
}
else
{
_persistedFile = null ;
_passphrase = null ;
}
}
/// <inheritdoc/>
public string Key => _provider . Key ;
/// <inheritdoc/>
public string DisplayName => _provider . DisplayName ;
/// <inheritdoc/>
public string Description => _provider . Description ;
2025-11-30 23:01:57 +01:00
/// <inheritdoc />
2025-12-01 15:29:24 +01:00
public Task < bool > IsSupported ( CancellationToken cancellationToken ) => _provider . IsSupported ( cancellationToken );
2025-11-30 23:01:57 +01:00
/// <inheritdoc />
public bool IsSetSupported => _provider . IsSetSupported ;
2024-10-24 15:56:39 +02:00
/// <inheritdoc/>
public IList < ICommandLineArgument > SupportedCommands => _provider . SupportedCommands ;
/// <inheritdoc/>
public async Task InitializeAsync ( System . Uri config , CancellationToken cancellationToken )
{
try
{
// Always initialize the provider, and use this if possible
await _provider . InitializeAsync ( config , cancellationToken ). ConfigureAwait ( false );
_initialized = true ;
}
catch
{
if ( _cachingLevel == CachingLevel . None )
throw ;
if ( _cachingLevel == CachingLevel . InMemory && ! _cache . ContainsKey ( _config ))
throw ;
if ( _cachingLevel == CachingLevel . Persistent )
{
await LoadCacheAsync ( cancellationToken ). ConfigureAwait ( false );
if (! _cache . ContainsKey ( _config ))
throw ;
}
}
}
/// <summary>
/// Loads the cache from disk, failing silently if the file could not be read
/// </summary>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
private async Task LoadCacheAsync ( CancellationToken cancellationToken )
{
bool tryLoad ;
lock ( _lock )
tryLoad = _cachingLevel == CachingLevel . Persistent && ! _cache . ContainsKey ( _config ) && File . Exists ( _persistedFile );
if ( tryLoad && ! string . IsNullOrEmpty ( _passphrase ))
{
// Load from disk
try
{
using ( var fs = new FileStream ( _persistedFile !, FileMode . Open , FileAccess . Read ))
using ( var ms = new MemoryStream ())
{
var decOpts = SharpAESCrypt . DecryptionOptions . Default with { LeaveOpen = true };
await SharpAESCrypt . AESCrypt . DecryptAsync ( _passphrase , fs , ms , decOpts , cancellationToken ). ConfigureAwait ( false );
ms . Position = 0 ;
var res = await System . Text . Json . JsonSerializer . DeserializeAsync < Dictionary < string , string >>( ms , cancellationToken : cancellationToken ). ConfigureAwait ( false )
?? throw new InvalidOperationException ( "Failed to deserialize the cache" );
lock ( _lock )
if (! _cache . ContainsKey ( _config ))
_cache [ _config ] = res ;
}
}
catch ( Exception ex )
{
Log . WriteWarningMessage ( LOGTAG , "LoadPersistedCacheError" , ex , "Failed to load cache from disk: {0}" , ex . Message );
}
}
}
/// <summary>
/// Saves the cache to disk
/// </summary>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
private async Task SaveCacheAsync ( CancellationToken cancellationToken )
{
bool trySave ;
lock ( _lock )
trySave = _initialized && _cachingLevel == CachingLevel . Persistent && _cache . ContainsKey ( _config );
if ( trySave && ! string . IsNullOrEmpty ( _passphrase ))
{
try
{
using ( var ms = new MemoryStream ())
{
Dictionary < string , string > data ;
lock ( _lock )
data = _cache [ _config ]. ToDictionary ( k => k . Key , k => k . Value );
await System . Text . Json . JsonSerializer . SerializeAsync ( ms , data , cancellationToken : cancellationToken ). ConfigureAwait ( false );
ms . Position = 0 ;
var encOpts = SharpAESCrypt . EncryptionOptions . Default ;
using ( var fs = new FileStream ( _persistedFile !, FileMode . Create , FileAccess . Write ))
await SharpAESCrypt . AESCrypt . EncryptAsync ( _passphrase , ms , fs , encOpts , cancellationToken ). ConfigureAwait ( false );
}
}
catch ( Exception ex )
{
Log . WriteWarningMessage ( LOGTAG , "SavePersistedCacheError" , ex , "Failed to save cache to disk: {0}" , ex . Message );
}
}
}
/// <summary>
/// Gets the cached values for the given keys
/// </summary>
/// <param name="keys">The keys to get</param>
/// <returns>The cached values or null if not found</returns>
private Dictionary < string , string >? GetFromCache ( IEnumerable < string > keys )
{
if ( _cachingLevel == CachingLevel . InMemory || _cachingLevel == CachingLevel . Persistent )
{
lock ( _lock )
if ( _cache . ContainsKey ( _config ) && keys . All ( x => _cache [ _config ]. ContainsKey ( x )))
return keys . ToDictionary ( k => k , k => _cache [ _config ][ k ]);
}
return null ;
}
/// <inheritdoc/>
public async Task < Dictionary < string , string >> ResolveSecretsAsync ( IEnumerable < string > keys , CancellationToken cancellationToken )
{
// Don't call the provider if it was not initialized
if (! _initialized )
{
var cached = GetFromCache ( keys );
if ( cached != null )
return cached ;
throw new InvalidOperationException ( "The provider has not been initialized" );
}
// Always call the provider to get fresh values, if it was initialized
Dictionary < string , string > result ;
try
{
result = await _provider . ResolveSecretsAsync ( keys , cancellationToken ). ConfigureAwait ( false );
}
catch
{
if ( _cachingLevel == CachingLevel . None )
throw ;
if ( _cachingLevel == CachingLevel . InMemory || _cachingLevel == CachingLevel . Persistent )
{
var cached = GetFromCache ( keys );
if ( cached != null )
return cached ;
}
throw ;
}
// We have a result, cache it
if ( _cachingLevel == CachingLevel . InMemory || _cachingLevel == CachingLevel . Persistent )
{
lock ( _lock )
{
if (! _cache . ContainsKey ( _config ))
{
_cache [ _config ] = result ;
}
else
{
foreach ( var k in result )
_cache [ _config ][ k . Key ] = k . Value ;
}
}
if ( _cachingLevel == CachingLevel . Persistent )
await SaveCacheAsync ( cancellationToken ). ConfigureAwait ( false );
}
return result ;
}
2025-11-30 23:01:57 +01:00
/// <inheritdoc />
public async Task SetSecretAsync ( string key , string value , bool overwrite , CancellationToken cancellationToken )
{
if (! _initialized )
throw new InvalidOperationException ( "The provider has not been initialized" );
await _provider . SetSecretAsync ( key , value , overwrite , cancellationToken ). ConfigureAwait ( false );
if ( _cachingLevel == CachingLevel . InMemory || _cachingLevel == CachingLevel . Persistent )
{
lock ( _lock )
{
if (! _cache . ContainsKey ( _config ))
_cache [ _config ] = new Dictionary < string , string >();
_cache [ _config ][ key ] = value ;
}
if ( _cachingLevel == CachingLevel . Persistent )
await SaveCacheAsync ( cancellationToken ). ConfigureAwait ( false );
}
}
2024-10-24 15:56:39 +02:00
}
}