// Copyright (C) 2025, The Duplicati Team
// 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.
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Duplicati.Library.Encryption;
using Duplicati.Library.Interface;
using Duplicati.Library.Main;
using Duplicati.Library.RemoteControl;
namespace Duplicati.Agent;
///
/// Settings for the agent
///
/// The filename of the settings
/// The JWT used for authentication
/// The URL of the server
/// The encryption key for the local settings
/// The server certificates
/// The encryption key to use for agent settings
public sealed record Settings(
string Filename,
string JWT,
string ServerUrl,
string CertificateUrl,
string? SettingsEncryptionKey,
IEnumerable ServerCertificates,
EncryptedFieldHelper.KeyInstance? Key
)
{
///
/// Gets the default settings file path
///
public static readonly string DefaultSettingsFile = GetSettingsPath();
///
/// Gets the default settings file name
///
public const string DefaultSettingsFilename = "agent.json";
///
/// The encryption key to use for agent settings
///
private static readonly EncryptedFieldHelper.KeyInstance? DefaultKey = null;
///
/// Shared JSON serialization options
///
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
PropertyNameCaseInsensitive = true
};
///
/// Loads the settings from a file
///
/// The path to the file
/// The loaded settings
public static Settings Load(string path, string? key)
{
var keyInstance = string.IsNullOrWhiteSpace(key)
? DefaultKey
: EncryptedFieldHelper.KeyInstance.CreateKey(key);
var tmp = (File.Exists(path) ? JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions) : null)
?? new Settings(path, string.Empty, string.Empty, string.Empty, string.Empty, Array.Empty(), null);
try
{
return tmp with
{
Filename = path,
JWT = EncryptedFieldHelper.Decrypt(tmp.JWT, keyInstance),
SettingsEncryptionKey = EncryptedFieldHelper.Decrypt(tmp.SettingsEncryptionKey, keyInstance),
Key = keyInstance
};
}
catch (SettingsEncryptionKeyMismatchException sek)
{
throw new UserInformationException("Invalid settings key provided", "InvalidAgentSettingsKey", sek);
}
catch (SettingsEncryptionKeyMissingException sek)
{
throw new UserInformationException("Settings file is encrypted but key is missing", "AgentSettingsKeyMissing", sek);
}
}
///
/// Saves the settings
///
public void Save()
{
var tmp = this with { Key = null, Filename = null! };
tmp = Key == null || Key.IsBlacklisted
? tmp
: tmp with
{
JWT = EncryptFieldIfPossible(JWT, Key),
SettingsEncryptionKey = EncryptFieldIfPossible(SettingsEncryptionKey, Key)
};
File.WriteAllText(Filename, JsonSerializer.Serialize(tmp, JsonOptions));
}
///
/// Encrypts a field if possible
///
/// The value to encrypt
/// The encrypted value
[return: NotNullIfNotNull("value")]
private static string? EncryptFieldIfPossible(string? value, EncryptedFieldHelper.KeyInstance? key)
{
if (key == null || key.IsBlacklisted)
return value;
if (value == null)
return null;
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
return EncryptedFieldHelper.Encrypt(value, key);
}
///
/// Gets the default settings file path
///
/// The path to the settings file
private static string GetSettingsPath()
{
// Ideally, this should use DataFolderManager.DATAFOLDER, but we cannot due to backwards compatibility
var folder = Library.AutoUpdater.DataFolderLocator.GetDefaultStorageFolder(DefaultSettingsFilename, true);
return Path.Combine(folder, DefaultSettingsFilename);
}
}