diff --git a/Source/LibationFileManager/AudibleFileStorage.cs b/Source/LibationFileManager/AudibleFileStorage.cs index 0bc3dc8e..ed852733 100644 --- a/Source/LibationFileManager/AudibleFileStorage.cs +++ b/Source/LibationFileManager/AudibleFileStorage.cs @@ -31,8 +31,19 @@ public abstract class AudibleFileStorage { try { - return (DateTime.UtcNow - lastInProgressFail) < RetryInProgressInterval ? null - : new DirectoryInfo(Configuration.Instance.InProgress).CreateSubdirectoryEx(subDirectory); + if ((DateTime.UtcNow - lastInProgressFail) < RetryInProgressInterval) + return null; + + var parent = new DirectoryInfo(Configuration.Instance.InProgress); + var dir = parent.CreateSubdirectoryEx(subDirectory); + + // On Unix, lock down the parent and the subdir to the current user (0700). + // Prevents other local users from reading partial downloads / decrypted artifacts + // when InProgress lives on a world-traversable filesystem like /tmp. + TrySetOwnerOnlyMode(parent.FullName); + TrySetOwnerOnlyMode(dir.FullName); + + return dir; } catch (Exception ex) { @@ -42,6 +53,20 @@ public abstract class AudibleFileStorage } } + private static void TrySetOwnerOnlyMode(string path) + { + if (OperatingSystem.IsWindows()) + return; + try + { + File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + } + catch + { + // best-effort. user may have intentionally set their own perms, or the FS may not support it. + } + } + public static LongPath? DownloadsInProgressDirectory => CreateInProgressDirectory("DownloadsInProgress")?.FullName; public static LongPath? DecryptInProgressDirectory => CreateInProgressDirectory("DecryptInProgress")?.FullName; diff --git a/Source/LibationFileManager/Configuration.KnownDirectories.cs b/Source/LibationFileManager/Configuration.KnownDirectories.cs index 30368246..f2b037d5 100644 --- a/Source/LibationFileManager/Configuration.KnownDirectories.cs +++ b/Source/LibationFileManager/Configuration.KnownDirectories.cs @@ -13,7 +13,10 @@ public partial class Configuration public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LibationFiles.LIBATION_FILES_KEY)); public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation")); public static string MyMusic => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic), "Libation")); - public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation")); + // Use a per-user subdir so we don't collide with -- or get blocked by -- another local + // user's leftover dir on shared-/tmp systems (Linux). On Windows and macOS, GetTempPath + // already returns a per-user location, so this is just a harmless extra path segment. + public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"Libation-{Environment.UserName}")); public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation")); public static string LocalAppData => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation")); diff --git a/Source/LibationUiBase/Upgrader.cs b/Source/LibationUiBase/Upgrader.cs index dfd28477..cf1f82ee 100644 --- a/Source/LibationUiBase/Upgrader.cs +++ b/Source/LibationUiBase/Upgrader.cs @@ -62,8 +62,9 @@ public class Upgrader : UpgraderBase } //Silently download the upgrade in the background, save it to a temp file. - - var zipFile = Path.Combine(Path.GetTempPath(), Path.GetFileName(upgradeProperties.ZipUrl)); + var zipFile = GetUpgradeDownloadPath(upgradeProperties.ZipUrl); + if (zipFile is null) + return null; Serilog.Log.Logger.Information($"Downloading {zipFile}"); @@ -103,6 +104,29 @@ public class Upgrader : UpgraderBase return null; } } + + /// + /// Allocate a fresh per-run temp directory for the upgrade zip and return the full path + /// the zip should be downloaded to. Uses a random subdirectory name (and 0700 perms on + /// Unix) so we never extract or execute from a predictable, shared-temp location. + /// + /// Destination path for the upgrade zip, or null if the temp directory + /// could not be created (in which case the upgrade-failed event has already been raised). + private string? GetUpgradeDownloadPath(string zipUrl) + { + try + { + var stagingDir = Directory.CreateTempSubdirectory("Libation-upgrade-").FullName; + return Path.Combine(stagingDir, Path.GetFileName(zipUrl)); + } + catch (Exception ex) + { + var message = "Failed to create a temp directory for the upgrade download."; + Serilog.Log.Logger.Error(ex, message); + OnUpgradeFailed(message, ex); + return null; + } + } } public class MockUpgrader : UpgraderBase