Update SharpAESCrypt to v3

This PR updates SharpAESCrypt to version 3.0.0.

With this change the written format for encrypted fields in the database and the remote encrypted files will be changed to Stream Format v3, which improves the key deriviation function and includes additional safeguards regarding content length.

The library still supports reading v2 files, but will write all new files as v3.

Environment variables, as well as module options, can toggle this behavior.

# Environment variable overview
- `DUPLICATI__AES_VERSION`: The file format version, either 3 (default) or 2
-`DUPLICATI__AES_V3_ITERATIONS`: The number of Pbkdf iterations to apply (default 300k)
- `DUPLICATI__AES_MINIMAL_HEADER`: Writes AES files with a minimal header (saves ~100 bytes per file)
- `DUPLICATI__AES_IGNORE_PADDING_BYTES`: Legacy setting for reading files written by the mainline AESCrypt tool

# A note on iterations
The iterations protect a weak key by adding additional computational overhead, but only adds overhead if the key is already providing at least 256 bits of high entropy.

For low-power devices, such as 32-bit RPI, this can be set as low as 10k, which will lower encryption and decryption time.  Beware that setting a low iteration count reduces security if the key is not sufficiently strong.
This commit is contained in:
Kenneth Skovhede
2026-03-13 17:20:33 +01:00
parent 45acf1a9bc
commit e98c48e078
3 changed files with 138 additions and 46 deletions
+131 -45
View File
@@ -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
/// </summary>
public class AESEncryption : EncryptionBase
{
/// <summary>
/// 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
/// </summary>
private const string KEY_AES_IGNORE_PADDING_BYTES = "aes-ignore-padding-bytes";
/// <summary>
/// The key used to define the AES stream format
/// </summary>
private const string KEY_AES_VERSION = "aes-version";
/// <summary>
/// The key used to define the number of iterations used for the AES stream format v3
/// </summary>
private const string KEY_AES_V3_ITERATIONS = "aes-v3-iterations";
/// <summary>
/// The key used to define if the AES output should use a minimal header
/// </summary>
private const string KEY_AES_MINIMAL_HEADER = "aes-minimal-header";
/// <summary>
/// The key used to encrypt the data
/// </summary>
@@ -41,15 +60,10 @@ namespace Duplicati.Library.Encryption
/// </summary>
private static long m_cachedsizeoverhead = -1;
/// <summary>
/// Cached set of options for minimal header
/// </summary>
private static readonly SharpAESCrypt.EncryptionOptions m_minimalHeaderOptions = new(InsertCreatedByIdentifier: false, InsertTimeStamp: false, InsertPlaceholder: false);
/// <summary>
/// Cached set of options for decryption
/// </summary>
private static readonly SharpAESCrypt.DecryptionOptions m_decryptionOptions = new(IgnorePaddingBytes: Environment.GetEnvironmentVariable("AES_IGNORE_PADDING_BYTES") == "1");
private readonly SharpAESCrypt.DecryptionOptions m_decryptionOptions;
/// <summary>
/// Options to use for encryption
@@ -61,6 +75,9 @@ namespace Duplicati.Library.Encryption
/// </summary>
public AESEncryption()
{
m_key = null!;
m_encryptionOptions = default!;
m_decryptionOptions = default!;
}
/// <summary>
@@ -69,22 +86,79 @@ namespace Duplicati.Library.Encryption
/// <param name="passphrase">The passphrase to use</param>
/// <param name="minimalheader">Flag controlling if the encryption is done with a minimal header</param>
public AESEncryption(string passphrase, bool minimalheader)
: this(passphrase, new Dictionary<string, string>() { { 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;
}
/// <summary>
/// Constructs a new AES encryption/decyption instance
/// </summary>
public AESEncryption(string passphrase, Dictionary<string, string> options)
: this(passphrase, false)
public AESEncryption(string passphrase, IReadOnlyDictionary<string, string> 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;
}
/// <summary>
/// Gets the environment variable value for an option
/// </summary>
/// <param name="key">The key to get the value for</param>
/// <returns>The value</returns>
private static string? GetEnvValue(string key)
=> Environment.GetEnvironmentVariable("DUPLICATI__" + key.ToUpperInvariant().Replace('-', '_'));
#region IEncryption Members
/// <summary>
@@ -145,19 +219,31 @@ namespace Duplicati.Library.Encryption
/// Gets a list of supported commandline arguments
/// </summary>
/// <value>The supported commands.</value>
public override IList<ICommandLineArgument> SupportedCommands
=> new List<ICommandLineArgument>([
new CommandLineArgument(
"aes-set-threadlevel",
CommandLineArgument.ArgumentType.String,
Strings.AESEncryption.AessetthreadlevelShort,
Strings.AESEncryption.AessetthreadlevelLong,
"",
null,
null,
Strings.AESEncryption.AessetthreadlevelDeprecated
),
]);
public override IList<ICommandLineArgument> 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
}
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpAESCrypt" Version="2.0.3" />
<PackageReference Include="SharpAESCrypt" Version="3.0.0" />
</ItemGroup>
<ItemGroup>
+6
View File
@@ -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
{