From d1fc6e98503006c9fb70d8b2074f36b82d8af854 Mon Sep 17 00:00:00 2001 From: yair100 <39923744+yair100@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:05:27 -0400 Subject: [PATCH] Fix: Ask for the archive password only once when copying out of an encrypted archive --- .../IPasswordProtectedItem.cs | 8 ++++++- .../Storage/StorageItems/ZipStorageFile.cs | 17 ++++++++++--- .../Storage/StorageItems/ZipStorageFolder.cs | 24 +++++++++++++++---- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Files.App/Utils/Storage/StorageBaseItems/IPasswordProtectedItem.cs b/src/Files.App/Utils/Storage/StorageBaseItems/IPasswordProtectedItem.cs index 7278d390cd27..72b7f3f42e76 100644 --- a/src/Files.App/Utils/Storage/StorageBaseItems/IPasswordProtectedItem.cs +++ b/src/Files.App/Utils/Storage/StorageBaseItems/IPasswordProtectedItem.cs @@ -12,6 +12,9 @@ public interface IPasswordProtectedItem Func> PasswordRequestedCallback { get; set; } + // Called after an operation succeeds with credentials obtained from PasswordRequestedCallback + void OnCredentialsVerified() { } + async Task RetryWithCredentialsAsync(Func> func, Exception exception) { var handled = exception is SevenZipOpenFailedException szofex && szofex.Result is OperationResult.WrongPassword || @@ -23,7 +26,9 @@ exception is ExtractionFailedException efex && efex.Result is OperationResult.Wr Credentials = await PasswordRequestedCallback(this); - return await func(); + var result = await func(); + OnCredentialsVerified(); + return result; } async Task RetryWithCredentialsAsync(Func func, Exception exception) @@ -38,6 +43,7 @@ exception is ExtractionFailedException efex && efex.Result is OperationResult.Wr Credentials = await PasswordRequestedCallback(this); await func(); + OnCredentialsVerified(); } void CopyFrom(IPasswordProtectedItem parent) diff --git a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs index 373adc2e62c6..be8b1c1bc18e 100644 --- a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs +++ b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFile.cs @@ -49,6 +49,17 @@ public override string DisplayType public Func> PasswordRequestedCallback { get; set; } + void IPasswordProtectedItem.OnCredentialsVerified() + { + using var password = Credentials.SecurePassword; + ZipStorageFolder.CachedCredentials[containerPath] = new StorageCredential(Credentials.UserName, password); + } + + private string ArchivePassword + => string.IsNullOrEmpty(Credentials.Password) && ZipStorageFolder.CachedCredentials.TryGetValue(containerPath, out var cached) + ? cached.Password + : Credentials.Password; + public ZipStorageFile(string path, string containerPath) { Name = IO.Path.GetFileName(path.TrimEnd('\\', '/')); @@ -329,7 +340,7 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption compressor.CustomParameters.Add("cu", "on"); compressor.SetFormatFromExistingArchive(archiveStream); var fileName = IO.Path.GetRelativePath(containerPath, IO.Path.Combine(IO.Path.GetDirectoryName(Path), desiredName)); - await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, fileName } }, Credentials.Password, ms); + await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, fileName } }, ArchivePassword, ms); } await using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) @@ -378,7 +389,7 @@ public override IAsyncAction DeleteAsync(StorageDeleteOption option) SevenZipCompressor compressor = new SevenZipCompressor() { CompressionMode = CompressionMode.Append }; compressor.CustomParameters.Add("cu", "on"); compressor.SetFormatFromExistingArchive(archiveStream); - await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, null } }, Credentials.Password, ms); + await compressor.ModifyArchiveAsync(archiveStream, new Dictionary() { { index, null } }, ArchivePassword, ms); } await using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -463,7 +474,7 @@ private IAsyncOperation OpenZipFileAsync() return AsyncInfo.Run(async (cancellationToken) => { var zipFile = await OpenZipFileAsync(FileAccessMode.Read); - return zipFile is not null ? new SevenZipExtractor(zipFile, Credentials.Password) : null; + return zipFile is not null ? new SevenZipExtractor(zipFile, ArchivePassword) : null; }); } diff --git a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs index e393e758787c..a9d9ed49ef0f 100644 --- a/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs +++ b/src/Files.App/Utils/Storage/StorageItems/ZipStorageFolder.cs @@ -35,6 +35,20 @@ public sealed partial class ZipStorageFolder : BaseStorageFolder, ICreateFileWit public Func> PasswordRequestedCallback { get; set; } + // Verified credentials per archive file path, shared with ZipStorageFile; SecureString-backed like FtpManager.Credentials + internal static readonly ConcurrentDictionary CachedCredentials = new(StringComparer.OrdinalIgnoreCase); + + void IPasswordProtectedItem.OnCredentialsVerified() + { + using var password = Credentials.SecurePassword; + CachedCredentials[containerPath] = new StorageCredential(Credentials.UserName, password); + } + + private string ArchivePassword + => string.IsNullOrEmpty(Credentials.Password) && CachedCredentials.TryGetValue(containerPath, out var cached) + ? cached.Password + : Credentials.Password; + public ZipStorageFolder(string path, string containerPath) { Name = IO.Path.GetFileName(path.TrimEnd('\\', '/')); @@ -316,7 +330,7 @@ public override IAsyncOperation CreateFolderAsync(string desi compressor.CustomParameters.Add("cu", "on"); compressor.SetFormatFromExistingArchive(archiveStream); var fileName = IO.Path.GetRelativePath(containerPath, zipDesiredName); - await compressor.CompressStreamDictionaryAsync(archiveStream, new Dictionary() { { fileName, null } }, Credentials.Password, ms); + await compressor.CompressStreamDictionaryAsync(archiveStream, new Dictionary() { { fileName, null } }, ArchivePassword, ms); } await using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -371,7 +385,7 @@ public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption var folderDes = IO.Path.Combine(IO.Path.GetDirectoryName(folderKey), desiredName); var entriesMap = new Dictionary(index.Select(x => new KeyValuePair(x.Index, IO.Path.Combine(folderDes, IO.Path.GetRelativePath(folderKey, x.Key))))); - await compressor.ModifyArchiveAsync(archiveStream, entriesMap, Credentials.Password, ms); + await compressor.ModifyArchiveAsync(archiveStream, entriesMap, ArchivePassword, ms); } await using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -420,7 +434,7 @@ public override IAsyncAction DeleteAsync(StorageDeleteOption option) compressor.CustomParameters.Add("cu", "on"); compressor.SetFormatFromExistingArchive(archiveStream); var entriesMap = new Dictionary(index.Select(x => new KeyValuePair(x.Index, null))); - await compressor.ModifyArchiveAsync(archiveStream, entriesMap, Credentials.Password, ms); + await compressor.ModifyArchiveAsync(archiveStream, entriesMap, ArchivePassword, ms); } await using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) { @@ -581,7 +595,7 @@ private IAsyncOperation OpenZipFileAsync() return AsyncInfo.Run(async (cancellationToken) => { var zipFile = await OpenZipFileAsync(FileAccessMode.Read); - return zipFile is not null ? new SevenZipExtractor(zipFile, Credentials.Password) : null; + return zipFile is not null ? new SevenZipExtractor(zipFile, ArchivePassword) : null; }); } @@ -645,7 +659,7 @@ public IAsyncOperation CreateFileAsync(Stream contents, string compressor.CustomParameters.Add("cu", "on"); compressor.SetFormatFromExistingArchive(archiveStream); var fileName = IO.Path.GetRelativePath(containerPath, zipDesiredName); - await compressor.CompressStreamDictionaryAsync(archiveStream, new Dictionary() { { fileName, contents } }, Credentials.Password, ms); + await compressor.CompressStreamDictionaryAsync(archiveStream, new Dictionary() { { fileName, contents } }, ArchivePassword, ms); } await using (var archiveStream = await OpenZipFileAsync(FileAccessMode.ReadWrite)) {