From 5d387765af15e71d2fd90f228e3d0b722d2a9e68 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 4 Aug 2026 15:35:00 +0200 Subject: [PATCH] always check and restrict existing wallets and backup directories to owner only permissions --- .../com/sparrowwallet/sparrow/io/Storage.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index e5ee3f14..bae64cff 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -29,6 +29,7 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.regex.Matcher; @@ -41,6 +42,7 @@ public class Storage { private static final DateTimeFormatter BACKUP_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); private static final Pattern DATE_PATTERN = Pattern.compile(".+-([0-9]{14}?).*"); + private static final Set warnedDirectories = ConcurrentHashMap.newKeySet(); public static final String WALLETS_DIR = "wallets"; public static final String WALLETS_BACKUP_DIR = "backup"; @@ -486,12 +488,16 @@ public class Storage { File walletsBackupDir = new File(getWalletsDir(), WALLETS_BACKUP_DIR); if(!walletsBackupDir.exists()) { createOwnerOnlyDirectory(walletsBackupDir); + } else { + //Unlike the wallets directory below, this directory is always created by Sparrow, so restricting it restores the permissions it was created with + setOwnerOnlyDirectory(walletsBackupDir); } return walletsBackupDir; } public static File getWalletsDir() { + boolean defaultWalletsDir = false; File walletsDir = Config.get().getWalletsDir(); if(walletsDir != null) { if(!walletsDir.exists() && (walletsDir.getParentFile() == null || !walletsDir.getParentFile().exists() || !walletsDir.getParentFile().canWrite())) { @@ -501,9 +507,12 @@ public class Storage { } if(walletsDir == null) { walletsDir = new File(getDataDir(), WALLETS_DIR); + defaultWalletsDir = true; } if(!walletsDir.exists()) { createOwnerOnlyDirectory(walletsDir); + } else if(defaultWalletsDir) { + setOwnerOnlyDirectory(walletsDir); } return walletsDir; @@ -700,6 +709,32 @@ public class Storage { return false; } + public static void setOwnerOnlyDirectory(File directory) { + //A symlinked directory has a target outside the application directories that may be deliberately shared, so leave it alone + if(isWindows() || Files.isSymbolicLink(directory.toPath())) { + return; + } + + Set ownerOnly = getDirectoryOwnerOnlyPosixFilePermissions(); + Set currentPermissions; + try { + currentPermissions = Files.getPosixFilePermissions(directory.toPath()); + } catch(UnsupportedOperationException | IOException e) { + log.debug("Could not read permissions on directory " + directory.getAbsolutePath(), e); + return; + } + + if(!ownerOnly.equals(currentPermissions)) { + try { + Files.setPosixFilePermissions(directory.toPath(), ownerOnly); + } catch(IOException e) { + if(warnedDirectories.add(directory.getAbsolutePath())) { + log.warn("Could not restrict permissions on directory " + directory.getAbsolutePath() + ", it remains readable by other users", e); + } + } + } + } + public static boolean createOwnerOnlyFile(File file) { try { if(isWindows()) {