diff --git a/Duplicati/Library/Encryption/AESEncryption.cs b/Duplicati/Library/Encryption/AESEncryption.cs index 8d5c12bf2..804b6609b 100644 --- a/Duplicati/Library/Encryption/AESEncryption.cs +++ b/Duplicati/Library/Encryption/AESEncryption.cs @@ -1,24 +1,26 @@ -// 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 +// 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. +#nullable enable + using System; using System.Collections.Generic; using System.IO; @@ -31,6 +33,23 @@ namespace Duplicati.Library.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 /// @@ -41,15 +60,10 @@ namespace Duplicati.Library.Encryption /// private static long m_cachedsizeoverhead = -1; - /// - /// Cached set of options for minimal header - /// - private static readonly SharpAESCrypt.EncryptionOptions m_minimalHeaderOptions = new(InsertCreatedByIdentifier: false, InsertTimeStamp: false, InsertPlaceholder: false); - /// /// Cached set of options for decryption /// - private static readonly SharpAESCrypt.DecryptionOptions m_decryptionOptions = new(IgnorePaddingBytes: Environment.GetEnvironmentVariable("AES_IGNORE_PADDING_BYTES") == "1"); + private readonly SharpAESCrypt.DecryptionOptions m_decryptionOptions; /// /// Options to use for encryption @@ -61,6 +75,9 @@ namespace Duplicati.Library.Encryption /// public AESEncryption() { + m_key = null!; + m_encryptionOptions = default!; + m_decryptionOptions = default!; } /// @@ -69,22 +86,79 @@ namespace Duplicati.Library.Encryption /// 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() } }) { - if (string.IsNullOrEmpty(passphrase)) - throw new ArgumentException(Strings.AESEncryption.EmptyKeyError, nameof(passphrase)); - - m_key = passphrase; - m_encryptionOptions = minimalheader ? m_minimalHeaderOptions : default; } /// /// Constructs a new AES encryption/decyption instance /// - public AESEncryption(string passphrase, Dictionary options) - : this(passphrase, false) + 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 + }; + + 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 /// @@ -145,19 +219,31 @@ namespace Duplicati.Library.Encryption /// Gets a list of supported commandline arguments /// /// The supported commands. - public override IList SupportedCommands - => new List([ - new CommandLineArgument( - "aes-set-threadlevel", - CommandLineArgument.ArgumentType.String, - Strings.AESEncryption.AessetthreadlevelShort, - Strings.AESEncryption.AessetthreadlevelLong, - "", - null, - null, - Strings.AESEncryption.AessetthreadlevelDeprecated - ), - ]); + 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 } diff --git a/Duplicati/Library/Encryption/Duplicati.Library.Encryption.csproj b/Duplicati/Library/Encryption/Duplicati.Library.Encryption.csproj index 768e514ab..08f9db26b 100644 --- a/Duplicati/Library/Encryption/Duplicati.Library.Encryption.csproj +++ b/Duplicati/Library/Encryption/Duplicati.Library.Encryption.csproj @@ -8,7 +8,7 @@ - + diff --git a/Duplicati/Library/Encryption/Strings.cs b/Duplicati/Library/Encryption/Strings.cs index e5677f4af..f035693a8 100644 --- a/Duplicati/Library/Encryption/Strings.cs +++ b/Duplicati/Library/Encryption/Strings.cs @@ -31,6 +31,12 @@ namespace Duplicati.Library.Encryption.Strings public static string AessetthreadlevelLong { get { return LC.L(@"Use this option to set the thread level allowed for AES crypt operations."); } } public static string AessetthreadlevelShort { get { return LC.L(@"Set thread level utilized for crypting"); } } public static string AessetthreadlevelDeprecated { get { return LC.L(@"The option --{0} is no longer used and has been deprecated.", "aes-set-threadlevel"); } } + public static string AesversionShort { get { return LC.L(@"Set the AES file format version"); } } + public static string AesversionLong { get { return LC.L(@"Use this option to set the AES encryption file format version."); } } + public static string Aesv3iterationsShort { get { return LC.L(@"Set the number of iterations used for AES v3 key derivation"); } } + public static string Aesv3iterationsLong { get { return LC.L(@"Use this option to set the number of iterations used for AES v3 encryption. The default is using the library default."); } } + public static string AesminimalheaderShort { get { return LC.L(@"Use minimal header for AES encryption"); } } + public static string AesminimalheaderLong { get { return LC.L(@"Use this option to use a minimal header for AES encryption. This will reduce the size of the encrypted file, but will remove some additional metadata that is normally stored in the header."); } } } internal static class EncryptionBase {