Improve secret provider and license checker

This improves the secret provider by allowing a value of `default://` to get the system default provider (if one exists).

The license checker now supports multiple formats for the on-disk file.
This commit is contained in:
Kenneth Skovhede
2026-06-25 14:40:58 +02:00
parent 2d2a8b1490
commit a9d389ffab
6 changed files with 55 additions and 48 deletions
+5 -7
View File
@@ -102,9 +102,8 @@ public static class Program
/// <returns>The exit code.</returns>
private static async Task<int> RunTest(string secretUrl, string[] secrets)
{
var secretProvider = SecretProviderLoader.CreateInstance(secretUrl);
await secretProvider.InitializeAsync(new Uri(secretUrl), CancellationToken.None);
var result = await secretProvider.ResolveSecretsAsync(secrets, CancellationToken.None);
var secretProvider = await SecretProviderLoader.CreateInstanceAsync(secretUrl, true, CancellationToken.None).ConfigureAwait(false);
var result = await secretProvider.ResolveSecretsAsync(secrets, CancellationToken.None).ConfigureAwait(false);
Console.WriteLine("NOTE: Secret values are not displayed for security reasons");
Console.WriteLine("Secrets:");
@@ -140,7 +139,7 @@ public static class Program
{
if (string.IsNullOrWhiteSpace(secretUrl))
{
var defaultProvider = await SecretProviderLoader.GetDefaultSecretProviderForOperatingSystem(CancellationToken.None);
var defaultProvider = await SecretProviderLoader.GetDefaultSecretProviderForOperatingSystem(true, CancellationToken.None);
if (defaultProvider == null)
throw new UserInformationException("No working default secret provider found", "NoDefaultSecretProvider");
@@ -204,12 +203,11 @@ public static class Program
throw new UserInformationException("Secret values do not match", "SecretMismatch");
}
var secretProvider = SecretProviderLoader.CreateInstance(secretUrl);
await secretProvider.InitializeAsync(new Uri(secretUrl), CancellationToken.None);
var secretProvider = await SecretProviderLoader.CreateInstanceAsync(secretUrl, true, CancellationToken.None).ConfigureAwait(false);
await secretProvider.SetSecretAsync(key, value, overwrite, CancellationToken.None);
// Verify that the secret was stored correctly
var result = await secretProvider.ResolveSecretsAsync([key], CancellationToken.None);
var result = await secretProvider.ResolveSecretsAsync([key], CancellationToken.None).ConfigureAwait(false);
if (!result.ContainsKey(key) || result[key] != value)
throw new UserInformationException("Failed to verify that the secret was stored correctly", "SecretVerificationFailed");
Console.WriteLine($"Secret '{key}' stored.");
@@ -99,8 +99,6 @@ public sealed record Settings(
ISecretProvider? secretInstance = null;
if (!string.IsNullOrWhiteSpace(secretProvider))
{
var secretProviderInstance = SecretProviderLoader.CreateInstance(secretProvider);
// Map into expected structure
var opts = new Dictionary<string, string?>
{
@@ -117,8 +117,10 @@ public class SecretProviderLoader
/// Creates an instance of a secret provider
/// </summary>
/// <param name="config">The configuration string</param>
/// <param name="initialize">Whether to initialize the provider</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>The secret provider instance</returns>
public static ISecretProvider CreateInstance(string config)
public static async Task<ISecretProvider> CreateInstanceAsync(string config, bool initialize, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(config))
throw new ArgumentNullException(nameof(config));
@@ -145,6 +147,15 @@ public class SecretProviderLoader
config = result;
}
if (string.Equals(config, "default://", StringComparison.OrdinalIgnoreCase))
{
var defaultProvider = await GetDefaultSecretProviderForOperatingSystem(initialize, cancellationToken).ConfigureAwait(false);
if (defaultProvider == null)
throw new InvalidOperationException("No default secret provider is available for this system");
return defaultProvider;
}
var uri = new Uri(config);
var key = uri.Scheme;
@@ -154,6 +165,9 @@ public class SecretProviderLoader
if (Activator.CreateInstance(providerType.GetType()) is not ISecretProvider provider)
throw new InvalidOperationException($"Failed to create an instance of {providerType}");
if (initialize)
await provider.InitializeAsync(uri, cancellationToken).ConfigureAwait(false);
return provider;
}
@@ -161,37 +175,34 @@ public class SecretProviderLoader
/// Gets the default secret provider for the current operating system
/// </summary>
/// <returns>The secret provider or null if none is available</returns>
public static async Task<ISecretProvider?> GetDefaultSecretProviderForOperatingSystem(CancellationToken cancellationToken)
public static async Task<ISecretProvider?> GetDefaultSecretProviderForOperatingSystem(bool initialize, CancellationToken cancellationToken)
{
if (OperatingSystem.IsWindows())
{
var res = new WindowsCredentialManagerProvider();
await res.InitializeAsync(new Uri("wincred://"), cancellationToken);
if (initialize)
await res.InitializeAsync(new Uri("wincred://"), cancellationToken).ConfigureAwait(false);
return res;
}
if (OperatingSystem.IsMacOS())
{
var res = new MacOSKeyChainProvider();
await res.InitializeAsync(new Uri("keychain://"), cancellationToken);
if (initialize)
await res.InitializeAsync(new Uri("keychain://"), cancellationToken).ConfigureAwait(false);
return res;
}
if (OperatingSystem.IsLinux())
{
ISecretProvider tmp = new LibSecretLinuxProvider();
if (await tmp.IsSupported(cancellationToken))
var res = new LibSecretLinuxProvider();
if (await res.IsSupported(cancellationToken).ConfigureAwait(false))
{
var res = new LibSecretLinuxProvider();
await res.InitializeAsync(new Uri("libsecret://"), cancellationToken);
if (await res.DoesCollectionExist(cancellationToken))
if (!initialize)
return res;
}
tmp = new UnixPassProvider();
if (await tmp.IsSupported(cancellationToken))
{
await tmp.InitializeAsync(new Uri("pass://"), cancellationToken);
return tmp;
await res.InitializeAsync(new Uri("libsecret://"), cancellationToken).ConfigureAwait(false);
if (await res.DoesCollectionExist(cancellationToken).ConfigureAwait(false))
return res;
}
}
+3 -22
View File
@@ -71,25 +71,6 @@ public static class SecretProviderHelper
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>
/// <param name="pattern">The pattern to use for matching</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>The secret provider instance</returns>
public static async Task<ISecretProvider> CreateInstanceAsync(string config, CachingLevel cachingLevel, string persistedFolder, string salt, string pattern, CancellationToken cancelToken)
{
var provider = SecretProviderLoader.CreateInstance(config);
var sp = WrapWithCache(config, provider, cachingLevel, persistedFolder, salt, pattern);
await sp.InitializeAsync(new System.Uri(config), cancelToken).ConfigureAwait(false);
return sp;
}
/// <summary>
/// Wraps a secret provider with caching
/// </summary>
@@ -118,14 +99,14 @@ public static class SecretProviderHelper
var providerConfig = options.GetValueOrDefault("secret-provider");
if (!string.IsNullOrWhiteSpace(providerConfig))
{
var provider = SecretProviderLoader.CreateInstance(providerConfig);
var provider = await SecretProviderLoader.CreateInstanceAsync(providerConfig, true, cancellationToken).ConfigureAwait(false);
if (provider?.IsSetSupported == true)
return provider;
}
try
{
return await SecretProviderLoader.GetDefaultSecretProviderForOperatingSystem(cancellationToken).ConfigureAwait(false);
return await SecretProviderLoader.GetDefaultSecretProviderForOperatingSystem(true, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -161,7 +142,7 @@ public static class SecretProviderHelper
}
else
{
var newProvider = SecretProviderLoader.CreateInstance(provider);
var newProvider = await SecretProviderLoader.CreateInstanceAsync(provider, false, cancellationToken);
// Weak salt, but semi-static
string salt;
+5 -2
View File
@@ -72,9 +72,9 @@ public static class LicenseChecker
if (string.IsNullOrWhiteSpace(licenseKey))
throw new ArgumentException("License key cannot be null or empty", nameof(licenseKey));
if (licenseKey.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
if (licenseKey.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
var filePath = licenseKey[5..];
var filePath = licenseKey[7..];
if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("License file path cannot be null or empty", nameof(licenseKey));
if (!File.Exists(filePath))
@@ -104,6 +104,9 @@ public static class LicenseChecker
}
else
{
if (licenseKey.StartsWith("jwt:", StringComparison.OrdinalIgnoreCase))
licenseKey = licenseKey[4..];
using var httpClient = new HttpClient();
using var content = JsonContent.Create(new LicenseRequestData(licenseKey));
using var response = await httpClient.PostAsync(ServerUrl, content, cancellationToken);
@@ -7,6 +7,7 @@ namespace Duplicati.Proprietary.LicenseChecker;
public static class LicenseHelper
{
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType(typeof(LicenseHelper));
private static readonly object _licenseLock = new();
private static LicenseData? _cachedLicenseData;
private static string? _remoteClientLicenseKey;
@@ -59,8 +60,23 @@ public static class LicenseHelper
// Check for a license file in the installation directory (highest priority)
var keyfilepath = Path.Combine(UpdaterManager.INSTALLATIONDIR, "license.key");
if (File.Exists(keyfilepath))
{
// Fallback to assume we have a direct license key
key = $"file://{keyfilepath}";
try
{
// Probe if the file has base64 encoded data
var content = File.ReadAllText(keyfilepath);
if (content.TrimStart().StartsWith("base64:", StringComparison.OrdinalIgnoreCase) || content.TrimStart().StartsWith("jwt:", StringComparison.OrdinalIgnoreCase))
key = content;
}
catch (Exception ex)
{
Library.Logging.Log.WriteVerboseMessage(LOGTAG, "FailedToLoadLicenseFile", ex, "Failed to open the existing license file, check permissions");
}
}
// Check for a license key in the environment variables
if (string.IsNullOrWhiteSpace(key))
key = Environment.GetEnvironmentVariable("DUPLICATI_LICENSE_KEY");