fix: handle remaining unchecked File.delete() and Tor dir permission results

This commit is contained in:
davotoula
2026-07-04 15:52:33 +02:00
parent 2b9ffb4849
commit 27650f2f77
10 changed files with 45 additions and 22 deletions
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.commons.keystorage
import com.github.javakeyring.BackendNotSupportedException
import com.github.javakeyring.Keyring
import com.github.javakeyring.PasswordAccessException
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -277,7 +278,7 @@ actual class SecureKeyStorage private actual constructor() {
} finally {
// Clean up temp file if it still exists
if (tempFile.exists()) {
tempFile.delete()
tempFile.deleteOrWarn("SecureKeyStorage", "temp key file")
}
}
}
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.commons.service.upload
import com.vitorpamplona.amethyst.commons.service.upload.ImageReencoder.ReencodeResult
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult
@@ -144,8 +145,8 @@ class UploadOrchestrator(
// Eager cleanup of every intermediate. NonCancellable so a
// user-cancelled upload still cleans up its temps.
withContext(NonCancellable) {
strippedTemp?.delete()
reencodedTemp?.delete()
strippedTemp?.deleteOrWarn("UploadOrchestrator", "stripped temp")
reencodedTemp?.deleteOrWarn("UploadOrchestrator", "reencoded temp")
}
}
}
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage
import com.vitorpamplona.amethyst.commons.keystorage.SecureStorageException
import com.vitorpamplona.amethyst.commons.model.account.AccountInfo
import com.vitorpamplona.amethyst.commons.model.account.SignerType
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.amethyst.desktop.network.DesktopHttpClient
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
@@ -261,9 +262,9 @@ class AccountManager internal constructor(
suspend fun loadSavedAccount(): Result<AccountState.LoggedIn> =
try {
// Clean up legacy files (one-time, silent)
File(amethystDir, "last_account.txt").delete()
File(amethystDir, "bunker_uri.txt").delete()
File(amethystDir, "nwc_connection.txt").delete()
File(amethystDir, "last_account.txt").deleteOrWarn("AccountManager", "legacy file")
File(amethystDir, "bunker_uri.txt").deleteOrWarn("AccountManager", "legacy file")
File(amethystDir, "nwc_connection.txt").deleteOrWarn("AccountManager", "legacy file")
// Single source of truth: accounts.json.enc
val activeNpub =
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage
import com.vitorpamplona.amethyst.commons.model.account.AccountInfo
import com.vitorpamplona.amethyst.commons.model.account.AccountStorage
import com.vitorpamplona.amethyst.commons.model.account.SignerType
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.quartz.utils.Log
import java.io.File
import java.nio.file.Files
@@ -166,7 +167,7 @@ class DesktopAccountStorage(
val backup = File(file.parent, "accounts.json.enc.corrupt.${System.currentTimeMillis()}")
java.nio.file.Files
.copy(file.toPath(), backup.toPath())
file.delete()
file.deleteOrWarn("DesktopAccountStorage", "corrupt accounts file")
backup.absolutePath
} catch (_: Exception) {
null
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.desktop.relay
import com.vitorpamplona.amethyst.commons.service.BasicBundledInsert
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -276,7 +277,7 @@ class LocalRelayStore(
private fun deleteDbFiles(path: String) {
listOf("", "-wal", "-shm", "-journal").forEach { suffix ->
File(path + suffix).delete()
File(path + suffix).deleteOrWarn("LocalRelayStore", "relay db file")
}
}
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.desktop.service.drafts
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -174,7 +175,7 @@ class DesktopDraftStore(
suspend fun deleteDraft(slug: String) {
val safeSlug = sanitizeSlug(slug)
mutex.withLock {
File(draftsDir, "$safeSlug.md").delete()
File(draftsDir, "$safeSlug.md").deleteOrWarn("DesktopDraftStore", "draft file")
val index = loadIndexMap()
index.remove(safeSlug)
@@ -249,7 +250,7 @@ class DesktopDraftStore(
StandardCopyOption.REPLACE_EXISTING,
)
} finally {
if (tempFile.exists()) tempFile.delete()
if (tempFile.exists()) tempFile.deleteOrWarn("DesktopDraftStore", "temp draft file")
}
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.desktop.service.media
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.amethyst.desktop.network.DesktopHttpClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -137,7 +138,7 @@ object VideoThumbnailCache {
// a persistent cache hit (decoders may fail on every
// retry against a half-MP4). Discard so the next request
// re-downloads from scratch.
if (!downloaded.persistable) downloaded.file.delete()
if (!downloaded.persistable) downloaded.file.deleteOrWarn("VideoThumbnailCache", "truncated video chunk")
}
}
}
@@ -166,7 +167,7 @@ object VideoThumbnailCache {
val hash = sha1Hex(url)
val cached = File(downloadCacheDir, "$hash.mp4")
if (cached.length() > 0L) return Download(cached, persistable = true)
if (cached.exists()) cached.delete()
if (cached.exists()) cached.deleteOrWarn("VideoThumbnailCache", "empty cached chunk")
var wrote = false
var rangeHonored = false
@@ -181,7 +182,7 @@ object VideoThumbnailCache {
}
}
if (!wrote || cached.length() == 0L) {
cached.delete()
cached.deleteOrWarn("VideoThumbnailCache", "empty cached chunk")
return null
}
return Download(cached, persistable = rangeHonored)
@@ -46,6 +46,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.io.File
import java.nio.file.Files
import java.nio.file.attribute.PosixFilePermission
/**
* Desktop Tor daemon manager using kmp-tor.
@@ -190,13 +192,7 @@ class DesktopTorManager(
private fun desktopEnvironment(): TorRuntime.Environment {
val appDir = torDataDirectory()
appDir.mkdirs()
// Restrict permissions to owner only (700)
appDir.setReadable(false, false)
appDir.setReadable(true, true)
appDir.setWritable(false, false)
appDir.setWritable(true, true)
appDir.setExecutable(false, false)
appDir.setExecutable(true, true)
restrictToOwner(appDir)
return TorRuntime.Environment.Builder(
workDirectory = appDir.resolve("work"),
@@ -205,6 +201,24 @@ class DesktopTorManager(
) {}
}
/** Restricts [dir] to owner only (700) — Tor state includes onion keys. */
private fun restrictToOwner(dir: File) {
try {
Files.setPosixFilePermissions(
dir.toPath(),
setOf(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
),
)
} catch (e: UnsupportedOperationException) {
// Windows: no POSIX permissions; the user profile's NTFS ACLs apply instead.
} catch (e: Exception) {
Log.w("DesktopTorManager", "Could not restrict permissions on ${dir.absolutePath}", e)
}
}
/** OS-specific data directory for Tor. */
internal fun torDataDirectory(): File {
val osName = System.getProperty("os.name", "").lowercase()
@@ -68,6 +68,7 @@ import com.vitorpamplona.amethyst.commons.service.upload.CompressionQuality
import com.vitorpamplona.amethyst.commons.service.upload.UploadOrchestrator
import com.vitorpamplona.amethyst.commons.service.upload.UploadResult
import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
import com.vitorpamplona.amethyst.desktop.ImageCompressionStore
import com.vitorpamplona.amethyst.desktop.account.AccountState
@@ -282,7 +283,7 @@ fun ComposeNoteDialog(
// User opted out of the compressed
// version — drop the temp before we
// ship the original.
item.compressedFile.delete()
item.compressedFile.deleteOrWarn("ComposeNoteDialog", "compressed temp")
orchestrator.upload(
file = file,
alt = null,
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.service.upload.ImageFormatSniffer
import com.vitorpamplona.amethyst.commons.service.upload.ImageReencoder
import com.vitorpamplona.amethyst.commons.service.upload.ImageReencoder.PassReason
import com.vitorpamplona.amethyst.commons.service.upload.ImageReencoder.ReencodeResult
import com.vitorpamplona.amethyst.commons.util.deleteOrWarn
import java.io.File
import javax.imageio.ImageIO
@@ -163,7 +164,7 @@ suspend fun buildPreview(
fun cleanupPreviewTemps(items: List<PreviewItem>) {
items.forEach { item ->
if (item is PreviewItem.Reencoded) {
item.compressedFile.delete()
item.compressedFile.deleteOrWarn("CompressionPreview", "compressed temp")
}
}
}