diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt index 76620aeea7..bcfe4a3509 100644 --- a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/keystorage/SecureKeyStorage.kt @@ -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") } } } diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/UploadOrchestrator.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/UploadOrchestrator.kt index 1c214a4a94..058dc558d7 100644 --- a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/UploadOrchestrator.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/service/upload/UploadOrchestrator.kt @@ -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") } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt index b5c4e5598c..66b0924a06 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt @@ -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 = 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 = diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/DesktopAccountStorage.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/DesktopAccountStorage.kt index 40c32e2ad4..3f6ff53ec2 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/DesktopAccountStorage.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/DesktopAccountStorage.kt @@ -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 diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/relay/LocalRelayStore.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/relay/LocalRelayStore.kt index 2dea705f9c..566df9e006 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/relay/LocalRelayStore.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/relay/LocalRelayStore.kt @@ -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") } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/drafts/DesktopDraftStore.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/drafts/DesktopDraftStore.kt index ac0dbaade0..8271970177 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/drafts/DesktopDraftStore.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/drafts/DesktopDraftStore.kt @@ -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") } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VideoThumbnailCache.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VideoThumbnailCache.kt index 09cc733d95..be0e10bb3a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VideoThumbnailCache.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/media/VideoThumbnailCache.kt @@ -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) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/tor/DesktopTorManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/tor/DesktopTorManager.kt index 210d2f2777..e8881b6375 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/tor/DesktopTorManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/tor/DesktopTorManager.kt @@ -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() diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt index fedaa0a6c5..3c36d63e95 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt @@ -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, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/media/CompressionPreview.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/media/CompressionPreview.kt index 66698f4b92..75064dd991 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/media/CompressionPreview.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/media/CompressionPreview.kt @@ -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) { items.forEach { item -> if (item is PreviewItem.Reencoded) { - item.compressedFile.delete() + item.compressedFile.deleteOrWarn("CompressionPreview", "compressed temp") } } }