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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user