// Copyright (C) 2026, 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. #nullable enable using System; using System.Collections.Generic; using System.IO; using Duplicati.Library.Interface; namespace Duplicati.Library.Encryption { /// /// Implements AES encryption /// public class AESEncryption : EncryptionBase { /// /// Toggles ignoring AES padding bytes; NOTE: this is not exposed as an option, but can be set using the environment variable DUPLICATI__AES_IGNORE_PADDING_BYTES /// private const string KEY_AES_IGNORE_PADDING_BYTES = "aes-ignore-padding-bytes"; /// /// The key used to define the AES stream format /// private const string KEY_AES_VERSION = "aes-version"; /// /// The key used to define the number of iterations used for the AES stream format v3 /// private const string KEY_AES_V3_ITERATIONS = "aes-v3-iterations"; /// /// The key used to define if the AES output should use a minimal header /// private const string KEY_AES_MINIMAL_HEADER = "aes-minimal-header"; /// /// The key used to encrypt the data /// private readonly string m_key; /// /// The cached value for size overhead /// private static long m_cachedsizeoverhead = -1; /// /// Cached set of options for decryption /// private readonly SharpAESCrypt.DecryptionOptions m_decryptionOptions; /// /// Options to use for encryption /// private readonly SharpAESCrypt.EncryptionOptions m_encryptionOptions; /// /// Default constructor, used to read file extension and supported commands /// public AESEncryption() { m_key = null!; m_encryptionOptions = default!; m_decryptionOptions = default!; } /// /// Constructs a new AES encryption/decyption instance /// /// The passphrase to use /// Flag controlling if the encryption is done with a minimal header public AESEncryption(string passphrase, bool minimalheader) : this(passphrase, new Dictionary() { { KEY_AES_MINIMAL_HEADER, minimalheader.ToString() } }) { } /// /// Constructs a new AES encryption/decyption instance /// public AESEncryption(string passphrase, IReadOnlyDictionary options) { if (string.IsNullOrEmpty(passphrase)) throw new ArgumentException(Strings.AESEncryption.EmptyKeyError, nameof(passphrase)); m_key = passphrase; var encOpts = SharpAESCrypt.EncryptionOptions.Default; var decOpts = SharpAESCrypt.DecryptionOptions.Default; int? version = null; int? iterations = null; var minimalHeader = Utility.Utility.ParseBool( options.GetValueOrDefault(KEY_AES_MINIMAL_HEADER), Utility.Utility.ParseBool( GetEnvValue(KEY_AES_MINIMAL_HEADER), false ) ); var ignorePaddingBytes = Utility.Utility.ParseBool( options.GetValueOrDefault(KEY_AES_IGNORE_PADDING_BYTES), Utility.Utility.ParseBool( GetEnvValue(KEY_AES_IGNORE_PADDING_BYTES), false ) ); if (int.TryParse(GetEnvValue(KEY_AES_VERSION), out var tempInt)) version = tempInt; if (int.TryParse(options.GetValueOrDefault(KEY_AES_VERSION), out tempInt)) version = tempInt; if (int.TryParse(GetEnvValue(KEY_AES_V3_ITERATIONS), out tempInt)) iterations = tempInt; if (int.TryParse(options.GetValueOrDefault(KEY_AES_V3_ITERATIONS), out tempInt)) iterations = tempInt; if (minimalHeader) encOpts = encOpts with { InsertCreatedByIdentifier = false, InsertTimeStamp = false, InsertPlaceholder = false }; // Until the next stable release, use version 2 by default if (version == null) version = 2; if (version.HasValue) encOpts = encOpts with { FileVersion = (byte)version.Value }; if (iterations.HasValue) encOpts = encOpts with { KdfIterations = iterations.Value }; if (ignorePaddingBytes) decOpts = decOpts with { IgnorePaddingBytes = true }; m_encryptionOptions = encOpts; m_decryptionOptions = decOpts; } /// /// Gets the environment variable value for an option /// /// The key to get the value for /// The value private static string? GetEnvValue(string key) => Environment.GetEnvironmentVariable("DUPLICATI__" + key.ToUpperInvariant().Replace('-', '_')); #region IEncryption Members /// /// The extension that the encryption implementation adds to the filename /// /// The filename extension. public override string FilenameExtension { get { return "aes"; } } /// /// A localized description of the encryption module /// /// The description. public override string Description { get { return string.Format(Strings.AESEncryption.Description_v2); } } /// /// A localized string describing the encryption module with a friendly name /// /// The display name. public override string DisplayName { get { return Strings.AESEncryption.DisplayName; } } /// /// Dispose the specified disposing. /// /// If set to true disposing. protected override void Dispose(bool disposing) { } /// /// Returns the size in bytes of the overhead that will be added to a file of the given size when encrypted /// /// The size of the file to encrypt /// The size of the overhead in bytes public override long SizeOverhead(long filesize) { if (m_cachedsizeoverhead != -1) return m_cachedsizeoverhead; //If we use 1, we trigger the blocksize. //As the AES algorithm does not alter the size, // the results are the same as for the real size, // but a single byte encryption is much faster. return m_cachedsizeoverhead = base.SizeOverhead(1); } /// /// Encrypts the stream /// /// The target stream /// An encrypted stream that can be written to public override Stream Encrypt(Stream input) => new SharpAESCrypt.EncryptingStream(m_key, input, m_encryptionOptions); /// /// Decrypts the stream to the output stream /// /// The encrypted stream /// The unencrypted stream public override Stream Decrypt(Stream input) => new SharpAESCrypt.DecryptingStream(m_key, input, m_decryptionOptions); /// /// Gets a list of supported commandline arguments /// /// The supported commands. public override IList SupportedCommands => [ new CommandLineArgument( KEY_AES_VERSION, CommandLineArgument.ArgumentType.Enumeration, Strings.AESEncryption.AesversionShort, Strings.AESEncryption.AesversionLong, SharpAESCrypt.EncryptionOptions.Default.FileVersion.ToString(), null, ["2", "3"] ), new CommandLineArgument( KEY_AES_V3_ITERATIONS, CommandLineArgument.ArgumentType.Integer, Strings.AESEncryption.Aesv3iterationsShort, Strings.AESEncryption.Aesv3iterationsLong, SharpAESCrypt.EncryptionOptions.Default.KdfIterations.ToString() ), new CommandLineArgument( KEY_AES_MINIMAL_HEADER, CommandLineArgument.ArgumentType.Boolean, Strings.AESEncryption.AesminimalheaderShort, Strings.AESEncryption.AesminimalheaderLong, "false" ) ]; #endregion } }