From a84e4ce08590c37d807fa830c4141dd79c1b295e Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 28 Sep 2026 13:59:14 -0300 Subject: [PATCH] desktop: run as the current user again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the dedicated-OS-user setup: Amber desktop no longer creates an amber user, no sudo dialog on first open, and every launch runs in the current session like before. XWayland's same-uid-only policy needed a per-launch xhost grant plus a pacman install inside the root setup, and the moving parts never settled into something the window survived — isolation is better served by Omarchy's Yama ptrace_scope plus a hardened user service (the Opal pattern) if we revisit this later. Kept from this run: the mandatory passphrase with its first-run gate, the 1-hour default auto-lock, passphrase-gated logout, and the bounded tray init that no longer lets a missing tray host stall startup. The d_dedicated_user_* strings are dropped from all 14 locales and the user-service idea is documented nowhere on purpose. Left over on machines that ran the first-open setup (root, one-off): pkill the amber user's instances, remove /usr/local/bin/amber-runas-amber and /etc/sudoers.d/amber-runas-amber, userdel -r amber, and drop the u:amber ACL from the invoking home. --- desktop/README.md | 35 +-- .../greenart7c3/nostrsigner/desktop/Main.kt | 18 -- .../nostrsigner/desktop/core/DedicatedUser.kt | 264 ------------------ .../nostrsigner/desktop/ui/UserSetupScreen.kt | 139 --------- .../src/main/resources/i18n/strings_de.xml | 5 - .../src/main/resources/i18n/strings_en.xml | 5 - .../src/main/resources/i18n/strings_es.xml | 5 - .../src/main/resources/i18n/strings_fr.xml | 5 - .../src/main/resources/i18n/strings_in.xml | 5 - .../src/main/resources/i18n/strings_it.xml | 5 - .../src/main/resources/i18n/strings_ja.xml | 5 - .../src/main/resources/i18n/strings_ko.xml | 5 - .../src/main/resources/i18n/strings_pt-BR.xml | 5 - .../src/main/resources/i18n/strings_ru.xml | 5 - .../src/main/resources/i18n/strings_th.xml | 5 - .../src/main/resources/i18n/strings_tr.xml | 5 - .../src/main/resources/i18n/strings_vi.xml | 5 - .../src/main/resources/i18n/strings_zh.xml | 5 - .../nostrsigner/desktop/DedicatedUserTest.kt | 145 ---------- 19 files changed, 1 insertion(+), 670 deletions(-) delete mode 100644 desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/core/DedicatedUser.kt delete mode 100644 desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/ui/UserSetupScreen.kt delete mode 100644 desktop/src/test/kotlin/com/greenart7c3/nostrsigner/desktop/DedicatedUserTest.kt diff --git a/desktop/README.md b/desktop/README.md index 8d64ef09..fa817e03 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -39,9 +39,7 @@ for the JVM) and mirrors the mobile UI and permission model. notification daemon (mako, dunst, swaync, GNOME Shell, …) via `notify-send` or `gdbus` on Linux — so they work on Hyprland/Wayland — `osascript` on macOS, and the AWT tray notification on Windows -- Mandatory passphrase lock (see Key storage below); on Linux Amber - additionally runs under its own dedicated OS user, set up in-app on first - open (see Running under a dedicated user below) +- Mandatory passphrase lock (see Key storage below) - Native desktop layout: sidebar navigation with an account switcher, dense list views, and keyboard shortcuts - Light/dark theme using the Amber palette @@ -159,37 +157,6 @@ jpackage can only produce installers for the OS it runs on, so release builds are made per-platform. Linux packaging needs `fakeroot` (deb) or `rpm-build` (rpm) installed. -### Running under a dedicated user (Linux) - -On Linux, Amber does not run as your login user: the first time it opens, it -asks for your password (sudo), creates a dedicated OS user, and re-launches -itself under that user. The process that holds your keys is then walled off -from the rest of your desktop session by the OS — other apps can no longer -read Amber's memory or files, closing the same-user-malware residual risk -described above (a process running as that user can still be attacked, of -course — this is isolation, not a security boundary against root). - -What the first-open setup does (as root, once): - -1. creates the dedicated user `amber` with its own home directory - (`AMBER_USER=name` picks a different name), -2. moves your existing Amber data (`~/.local/share/amber`) into that home, -3. installs a root-owned launcher (`/usr/local/bin/amber-runas-`) that - execs exactly the Amber binary with only your session's socket locations - (Wayland, X11/XWayland, D-Bus for tray and notifications) passed as - arguments — never arbitrary code or environment, -4. installs a narrow sudoers rule (`/etc/sudoers.d/amber-runas-`, - validated with `visudo`) allowing your user to run that launcher as the - dedicated user without a password, -5. re-launches Amber under the dedicated user. - -Afterwards every launch switches to the dedicated user silently. If the -installed binary path changes (reinstall, update), the next open asks for -your password once to regenerate the launcher. Set -`AMBER_DISABLE_DEDICATED_USER=1` to skip the whole flow (useful for -`./gradlew :desktop:run`, which is skipped automatically since it launches a -bare `java` binary), and requires the `acl` package for `setfacl`. - ## Tests ```bash diff --git a/desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/Main.kt b/desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/Main.kt index 40755085..873e9d05 100644 --- a/desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/Main.kt +++ b/desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/Main.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.window.rememberWindowState import com.greenart7c3.nostrsigner.desktop.core.AccountManager import com.greenart7c3.nostrsigner.desktop.core.AccountsStore import com.greenart7c3.nostrsigner.desktop.core.AmberDesktop -import com.greenart7c3.nostrsigner.desktop.core.DedicatedUser import com.greenart7c3.nostrsigner.desktop.core.DesktopAccount import com.greenart7c3.nostrsigner.desktop.core.Notifier import com.greenart7c3.nostrsigner.desktop.core.PassphraseLock @@ -30,7 +29,6 @@ import com.greenart7c3.nostrsigner.desktop.core.describe import com.greenart7c3.nostrsigner.desktop.ui.App import com.greenart7c3.nostrsigner.desktop.ui.NostrSignerTheme import com.greenart7c3.nostrsigner.desktop.ui.handleShortcut -import com.greenart7c3.nostrsigner.desktop.ui.runUserSetupWindow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -122,22 +120,6 @@ private object DesktopTray { } fun main() { - when (val state = DedicatedUser.detect()) { - DedicatedUser.State.Active -> startAmber() - is DedicatedUser.State.Ready -> - // A launcher is installed: switch to the dedicated user right away - // without asking for a password. If the switch fails, fall through - // to the setup window to regenerate it; this process exits either - // way once the window closes. - if (!DedicatedUser.relaunch(state.command)) { - runUserSetupWindow() - } - - is DedicatedUser.State.SetupNeeded -> runUserSetupWindow() - } -} - -private fun startAmber() { // The dorkbox tray prefers to be created before Compose/AWT initializes // GTK (dorkbox has to own GTK loading, otherwise the AppIndicator backend // fails to start and SystemTray.get() returns null even when diff --git a/desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/core/DedicatedUser.kt b/desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/core/DedicatedUser.kt deleted file mode 100644 index c4bcdb35..00000000 --- a/desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/core/DedicatedUser.kt +++ /dev/null @@ -1,264 +0,0 @@ -package com.greenart7c3.nostrsigner.desktop.core - -import java.io.File -import java.nio.file.Files -import java.nio.file.Path - -/** - * On Linux, Amber runs under a dedicated OS user instead of the login user: - * the process that holds your keys is then walled off from the rest of the - * desktop session by the OS itself (memory, files, and data directory). - * - * On first open (and whenever the setup went stale) Amber asks for the sudo - * password in-app and, as root: - * 1. creates the dedicated user with its own home directory, - * 2. moves the invoking user's Amber data into that home (once), - * 3. installs a root-owned launcher that runs the Amber binary as the - * dedicated user with only the session socket locations as arguments, - * 4. installs a narrow sudoers rule (this user -> launcher, NOPASSWD) so - * later launches switch to the dedicated user without asking again, - * 5. re-executes Amber under the dedicated user. - * - * The launcher never accepts arbitrary arguments or environment: it execs - * exactly the Amber binary it was generated for, so the sudoers rule cannot - * be reused to run anything else as the dedicated user. - * - * Escapes: `AMBER_DISABLE_DEDICATED_USER=1` skips the whole flow, and - * `AMBER_USER=name` picks a different OS user name. Bare `java` launches - * (`./gradlew :desktop:run`) are skipped — use the packaged image. - */ -object DedicatedUser { - private const val ENV_DISABLE = "AMBER_DISABLE_DEDICATED_USER" - private const val ENV_USER = "AMBER_USER" - - /** `$` for building shell scripts inside Kotlin templates. */ - private const val D = "$" - - private fun wrapperPath(name: String) = "/usr/local/bin/amber-runas-$name" - private fun sudoersPath(name: String) = "/etc/sudoers.d/amber-runas-$name" - - val isLinux: Boolean = System.getProperty("os.name").lowercase().let { - it.contains("linux") || it.contains("nix") || it.contains("nux") - } - - /** Name of the OS user that runs Amber. */ - val userName: String get() = System.getenv(ENV_USER) ?: "amber" - - sealed interface State { - /** Already running as the dedicated user, or the feature does not apply. */ - data object Active : State - - /** Needs (re-)setup; [stale] is true when only the launcher is outdated. */ - data class SetupNeeded(val stale: Boolean) : State - - /** A working launcher exists: switch without asking for a password. */ - data class Ready(val command: List) : State - } - - /** Inspects this process and returns what to do before starting the app. */ - fun detect(): State { - if (!isLinux || System.getenv(ENV_DISABLE) == "1") return State.Active - if (currentUserName() == userName) return State.Active - val exe = currentExecutable() ?: return State.Active - if (File(exe).name == "java") return State.Active // dev run under Gradle - - val wrapper = File(wrapperPath(userName)) - val userOk = userExists(userName) - val launcherOk = try { - wrapper.isFile && wrapper.canRead() && wrapper.readText().contains(exe) - } catch (_: Exception) { - false - } - return when { - userOk && launcherOk -> State.Ready(relaunchCommand(wrapper.absolutePath)) - else -> State.SetupNeeded(stale = userOk) - } - } - - /** - * Runs the sudo setup with the given password (fed to `sudo -S`) and - * returns true when the dedicated user, launcher, and sudoers rule are - * all in place. - */ - fun setup(password: CharArray): Boolean { - val session = sessionArgs() - return try { - val script = rootSetupScript( - name = userName, - currentUserName = currentUserName(), - exePath = currentExecutable() ?: return false, - wrapperPath = wrapperPath(userName), - sudoersPath = sudoersPath(userName), - dataDir = AppDirs.dataDir.absolutePath, - amberDataDir = amberDataDir(userName), - xdgRuntimeDir = session[0], - waylandDisplay = session[1], - x11Socket = x11SocketPath(session[2], session[0]), - ) - val process = ProcessBuilder("sudo", "-k", "-S", "-p", "", "bash", "-s") - .redirectErrorStream(true) - .start() - process.outputStream.use { out -> - out.write(String(password).toByteArray(Charsets.UTF_8)) - out.write('\n'.code) - out.write(script.toByteArray(Charsets.UTF_8)) - } - val output = process.inputStream.readBytes().toString(Charsets.UTF_8) - val ok = process.waitFor() == 0 - if (!ok) AmberLogger.d("DedicatedUser", "Setup failed: ${output.take(500)}") - ok - } catch (e: Exception) { - AmberLogger.d("DedicatedUser", "Setup failed: ${e.message}") - false - } finally { - password.fill('\u0000') - } - } - - /** - * Spawns [command] (the launcher as the dedicated user) and reports - * whether the child looks alive — the caller then exits this process. - */ - fun relaunch(command: List): Boolean = try { - val child = ProcessBuilder(command).start() - Thread.sleep(400) - child.isAlive - } catch (e: Exception) { - AmberLogger.d("DedicatedUser", "Relaunch failed: ${e.message}") - false - } - - /** The command to switch to the dedicated user for the current session. */ - fun relaunchCommand(wrapperPath: String): List = listOf("sudo", "-n", "-u", userName, wrapperPath) + sessionArgs() - - // ----- pure generators (unit-tested) ----- - - fun isValidUserName(name: String): Boolean = Regex("[a-z_][a-z0-9_-]{0,31}").matches(name) - - fun wrapperScript(exePath: String, home: String, name: String): String = """ - #!/bin/sh - # Generated by Amber; do not edit. - # Runs the Amber desktop app as its dedicated user. Only the session - # socket locations are passed as arguments — never arbitrary code or env. - set -e - XDG_RUNTIME_DIR='${D}1' - WAYLAND_DISPLAY='${D}2' - DISPLAY='${D}3' - DBUS_SESSION_BUS_ADDRESS='${D}4' - HOME='$home' - USER='$name' - LOGNAME='$name' - export XDG_RUNTIME_DIR WAYLAND_DISPLAY DISPLAY DBUS_SESSION_BUS_ADDRESS HOME USER LOGNAME - exec '$exePath' - """.trimIndent() + "\n" - - fun sudoersRule(invoker: String, name: String, wrapperPath: String): String = "$invoker ALL=($name) NOPASSWD: $wrapperPath\n" - - /** - * The root script piped to `sudo bash -s`. Every interpolated value is a - * system path or a validated user name; the launcher and sudoers payloads - * are written via quoted heredocs so nothing inside them is expanded. - */ - fun rootSetupScript( - name: String, - currentUserName: String, - exePath: String, - wrapperPath: String, - sudoersPath: String, - dataDir: String, - amberDataDir: String, - xdgRuntimeDir: String, - waylandDisplay: String, - x11Socket: String?, - ): String { - require(isValidUserName(name)) { "Invalid user name: $name" } - val sudoersTmp = "$sudoersPath.tmp" - val launcher = wrapperScript(exePath = exePath, home = homeOf(name), name = name) - val rule = sudoersRule(currentUserName, name, wrapperPath) - // Payloads are inserted AFTER trimIndent: interpolating them into the - // template would reset the common indentation to zero (their lines are - // flush-left), leaving the heredoc terminators indented and the - // heredocs swallowing the rest of the script. - return """ - set -e - if ! id -u '$name' >/dev/null 2>&1; then - nologin_bin="$D(command -v nologin || echo /bin/false)" - useradd --create-home --shell "${D}nologin_bin" '$name' - fi - if [ -d '$dataDir' ] && [ ! -e '$amberDataDir' ]; then - mkdir -p "$D(dirname '$amberDataDir')" - cp -a '$dataDir' '$amberDataDir' - # 'user:' sets the owner and defaults the group to the user's - # login group (avoids nested quoting around a command - # substitution here). - chown -R "$name:" '$amberDataDir' - fi - cat > '$wrapperPath' <<'WRAPPER_EOF' - AMBER_WRAPPER_PAYLOAD - WRAPPER_EOF - chown root:root '$wrapperPath' - chmod 0755 '$wrapperPath' - cat > '$sudoersTmp' <<'SUDOERS_EOF' - AMBER_SUDOERS_PAYLOAD - SUDOERS_EOF - chown root:root '$sudoersTmp' - chmod 0440 '$sudoersTmp' - if command -v visudo >/dev/null 2>&1; then - visudo -cf '$sudoersTmp' >/dev/null - fi - mv '$sudoersTmp' '$sudoersPath' - # Let the dedicated user reach this session's sockets: X11/XWayland, - # Wayland, and D-Bus (tray + notifications). - if command -v setfacl >/dev/null 2>&1; then - setfacl -m 'u:$name:rx' '$xdgRuntimeDir' || true - [ -S '$xdgRuntimeDir/$waylandDisplay' ] && setfacl -m 'u:$name:rw' '$xdgRuntimeDir/$waylandDisplay' || true - [ -n '${x11Socket ?: ""}' ] && [ -S '$x11Socket' ] && setfacl -m 'u:$name:rw' '$x11Socket' || true - [ -S '$xdgRuntimeDir/bus' ] && setfacl -m 'u:$name:rw' '$xdgRuntimeDir/bus' || true - fi - """.trimIndent() - .replace("AMBER_WRAPPER_PAYLOAD\n", launcher + "\n") - .replace("AMBER_SUDOERS_PAYLOAD\n", rule) + "\n" - } - - // ----- environment helpers ----- - - internal fun amberDataDir(name: String): String = Path.of(homeOf(name), ".local", "share", "amber").toString() - - internal fun x11SocketPath(display: String?, xdgRuntimeDir: String): String? { - if (display.isNullOrBlank() || !display.startsWith(":")) return null - val number = display.drop(1).substringBefore('.') - return "$xdgRuntimeDir/.X11-unix/X$number" - } - - private fun currentUserName(): String = System.getProperty("user.name") - - private fun currentExecutable(): String? = try { - Files.readSymbolicLink(Path.of("/proc/self/exe")).toString() - } catch (_: Exception) { - null - } - - private fun userExists(name: String): Boolean = try { - ProcessBuilder("getent", "passwd", name).start().waitFor() == 0 - } catch (_: Exception) { - false - } - - private fun homeOf(name: String): String = "/home/$name" - - internal fun sessionArgs(): List = listOf( - System.getenv("XDG_RUNTIME_DIR") ?: "/run/user/${uid()}", - System.getenv("WAYLAND_DISPLAY") ?: "", - System.getenv("DISPLAY") ?: "", - System.getenv("DBUS_SESSION_BUS_ADDRESS") ?: "", - ) - - private fun uid(): String = ProcessHandle.current().let { _ -> - try { - val lines = java.nio.file.Files.readAllLines(Path.of("/proc/self/status")) - lines.firstOrNull { it.startsWith("Uid:") }?.split(Regex("\\s+"))?.get(1) ?: "1000" - } catch (_: Exception) { - "1000" - } - } -} diff --git a/desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/ui/UserSetupScreen.kt b/desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/ui/UserSetupScreen.kt deleted file mode 100644 index ada7974f..00000000 --- a/desktop/src/main/kotlin/com/greenart7c3/nostrsigner/desktop/ui/UserSetupScreen.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.greenart7c3.nostrsigner.desktop.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.application -import androidx.compose.ui.window.rememberWindowState -import com.greenart7c3.nostrsigner.desktop.core.DedicatedUser -import com.greenart7c3.nostrsigner.desktop.core.Strings -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -/** - * Blocking first-open flow on Linux: asks for the sudo password, creates the - * dedicated OS user with its launcher + sudoers rule, and re-launches Amber - * under that user. Returns true when a dedicated-user process has been - * started (the caller must exit); false when the user closed the window - * without completing the setup. - */ -fun runUserSetupWindow(): Boolean { - var switched = false - application { - val windowState = rememberWindowState(width = 560.dp, height = 400.dp) - var password by remember { mutableStateOf("") } - var error by remember { mutableStateOf(null) } - var working by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() - val language by Strings.currentLanguage.collectAsState() - - Window( - onCloseRequest = ::exitApplication, - state = windowState, - visible = true, - title = "Amber", - icon = painterResource("icon.png"), - ) { - NostrSignerTheme { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(Modifier.widthIn(max = 480.dp).padding(24.dp)) { - Text( - Strings.get("d_dedicated_user_title", language), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.SemiBold, - ) - Spacer(Modifier.height(8.dp)) - Text( - Strings.format("d_dedicated_user_desc", DedicatedUser.userName, language = language), - style = MaterialTheme.typography.bodyMedium, - ) - Spacer(Modifier.height(16.dp)) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text(Strings.get("d_dedicated_user_password", language)) }, - singleLine = true, - visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - enabled = !working, - modifier = Modifier.fillMaxWidth(), - ) - error?.let { - Spacer(Modifier.height(8.dp)) - Text( - it, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - Spacer(Modifier.height(16.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - AmberButton( - text = if (working) Strings.get("d_working", language) else Strings.get("d_dedicated_user_set_up", language), - enabled = !working && password.isNotEmpty(), - onClick = { - working = true - error = null - scope.launch { - val ok = withContext(Dispatchers.IO) { - DedicatedUser.setup(password.toCharArray()) - } - password = "" - if (!ok) { - error = Strings.get("d_dedicated_user_failed", language) - working = false - } else { - val command = (DedicatedUser.detect() as? DedicatedUser.State.Ready)?.command - if (command != null && DedicatedUser.relaunch(command)) { - switched = true - exitApplication() - } else { - error = Strings.get("d_dedicated_user_failed", language) - working = false - } - } - } - }, - ) - AmberOutlinedButton( - text = Strings.get("cancel", language), - enabled = !working, - onClick = ::exitApplication, - ) - } - } - } - } - } - } - return switched -} diff --git a/desktop/src/main/resources/i18n/strings_de.xml b/desktop/src/main/resources/i18n/strings_de.xml index d17b3cb2..a4d2064c 100644 --- a/desktop/src/main/resources/i18n/strings_de.xml +++ b/desktop/src/main/resources/i18n/strings_de.xml @@ -788,11 +788,6 @@ Gib deine Passphrase ein, um dich abzumelden Amber benötigt eine Passphrase. Deine Schlüssel bleiben auf der Festplatte verschlüsselt, selbst gegenüber Software, die deine Dateien lesen kann – du wirst beim Start von Amber und nach jedem Sperren danach gefragt. Schütze deine Schlüssel - Amber unter einem eigenen Benutzer ausführen - Amber erstellt den Benutzer „%1$s“ und führt stets unter ihm aus, damit andere Apps auf diesem Computer nicht seinen Speicher oder seine Dateien lesen können. Deine vorhandenen Amber-Daten werden in dessen Home-Verzeichnis verschoben. Gib dein Passwort (sudo) ein, um das einzurichten. - Dein Passwort (sudo) - Einrichten und neu starten - Einrichtung fehlgeschlagen – prüfe dein Passwort und versuche es erneut Passphrase festlegen Jetzt sperren Passphrase ändern diff --git a/desktop/src/main/resources/i18n/strings_en.xml b/desktop/src/main/resources/i18n/strings_en.xml index a040583c..19c9fe76 100644 --- a/desktop/src/main/resources/i18n/strings_en.xml +++ b/desktop/src/main/resources/i18n/strings_en.xml @@ -799,11 +799,6 @@ Enter your passphrase to log out Amber requires a passphrase. Your keys stay encrypted at rest, even against software that can read your files — you will be asked for it when Amber starts and whenever it locks. Protect your keys - Run Amber under its own user - Amber will create the OS user “%1$s” and always run under it, so other apps on this computer cannot read its memory or files. Your existing Amber data is moved to that user’s home. Enter your password (sudo) to set this up. - Your password (sudo) - Set up and restart - Setup failed — check your password and try again Set a passphrase Lock now Change passphrase diff --git a/desktop/src/main/resources/i18n/strings_es.xml b/desktop/src/main/resources/i18n/strings_es.xml index f569ad5e..49807446 100644 --- a/desktop/src/main/resources/i18n/strings_es.xml +++ b/desktop/src/main/resources/i18n/strings_es.xml @@ -791,11 +791,6 @@ Introduce tu frase de contraseña para cerrar sesión Amber requiere una frase de contraseña. Tus claves permanecen cifradas en reposo, incluso frente a software que pueda leer tus archivos; se te pedirá al iniciar Amber y cada vez que se bloquee. Protege tus claves - Ejecutar Amber con su propio usuario - Amber creará el usuario del sistema «%1$s» y se ejecutará siempre con él, para que otras aplicaciones de este equipo no puedan leer su memoria ni sus archivos. Tus datos existentes de Amber se mueven a la carpeta personal de ese usuario. Introduce tu contraseña (sudo) para configurarlo. - Tu contraseña (sudo) - Configurar y reiniciar - La configuración falló; comprueba tu contraseña e inténtalo de nuevo Establecer una frase de contraseña Bloquear ahora Cambiar frase de contraseña diff --git a/desktop/src/main/resources/i18n/strings_fr.xml b/desktop/src/main/resources/i18n/strings_fr.xml index 64bc67ff..9db7082d 100644 --- a/desktop/src/main/resources/i18n/strings_fr.xml +++ b/desktop/src/main/resources/i18n/strings_fr.xml @@ -788,11 +788,6 @@ Saisissez votre phrase secrète pour vous déconnecter Amber exige une phrase secrète. Vos clés restent chiffrées au repos, même face à un logiciel capable de lire vos fichiers ; elle vous sera demandée au démarrage d'Amber et après chaque verrouillage. Protégez vos clés - Exécuter Amber sous son propre utilisateur - Amber va créer l’utilisateur système « %1$s » et s’exécutera toujours sous celui-ci, afin que les autres applications de cet ordinateur ne puissent pas lire sa mémoire ni ses fichiers. Vos données Amber existantes sont déplacées vers le dossier personnel de cet utilisateur. Saisissez votre mot de passe (sudo) pour configurer cela. - Votre mot de passe (sudo) - Configurer et redémarrer - Échec de la configuration — vérifiez votre mot de passe et réessayez Définir une phrase secrète Verrouiller maintenant Changer la phrase secrète diff --git a/desktop/src/main/resources/i18n/strings_in.xml b/desktop/src/main/resources/i18n/strings_in.xml index f1e4b2e0..167a0da8 100644 --- a/desktop/src/main/resources/i18n/strings_in.xml +++ b/desktop/src/main/resources/i18n/strings_in.xml @@ -791,11 +791,6 @@ Masukkan frasa sandi Anda untuk keluar Amber membutuhkan frasa sandi. Kunci Anda tetap terenkripsi saat disimpan, bahkan dari perangkat lunak yang dapat membaca berkas Anda — Anda akan dimintai frasa sandi saat Amber dimulai dan setiap kali terkunci. Lindungi kunci Anda - Jalankan Amber dengan penggunanya sendiri - Amber akan membuat pengguna OS “%1$s” dan selalu berjalan dengannya, sehingga aplikasi lain di komputer ini tidak dapat membaca memorinya atau berkasnya. Data Amber Anda yang ada dipindahkan ke direktori rumah pengguna tersebut. Masukkan kata sandi Anda (sudo) untuk menyiapkannya. - Kata sandi Anda (sudo) - Siapkan dan mulai ulang - Penyiapan gagal — periksa kata sandi Anda dan coba lagi Tetapkan frasa sandi Kunci sekarang Ubah frasa sandi diff --git a/desktop/src/main/resources/i18n/strings_it.xml b/desktop/src/main/resources/i18n/strings_it.xml index d917cc2e..88026314 100644 --- a/desktop/src/main/resources/i18n/strings_it.xml +++ b/desktop/src/main/resources/i18n/strings_it.xml @@ -791,11 +791,6 @@ Inserisci la tua passphrase per uscire Amber richiede una passphrase. Le tue chiavi restano cifrate a riposo, anche contro software in grado di leggere i tuoi file: ti verrà richiesta all'avvio di Amber e a ogni blocco. Proteggi le tue chiavi - Esegui Amber con un utente dedicato - Amber creerà l’utente di sistema “%1$s” e verrà eseguito sempre con esso, così le altre app di questo computer non potranno leggere la sua memoria o i suoi file. I tuoi dati Amber esistenti vengono spostati nella home di quell’utente. Inserisci la tua password (sudo) per configurarlo. - La tua password (sudo) - Configura e riavvia - Configurazione non riuscita — controlla la password e riprova Imposta una passphrase Blocca ora Cambia passphrase diff --git a/desktop/src/main/resources/i18n/strings_ja.xml b/desktop/src/main/resources/i18n/strings_ja.xml index 355029ae..1109618a 100644 --- a/desktop/src/main/resources/i18n/strings_ja.xml +++ b/desktop/src/main/resources/i18n/strings_ja.xml @@ -767,11 +767,6 @@ ログアウトするにはパスフレーズを入力してください Amber ではパスフレーズが必須です。ファイルを読み取れるソフトウェアに対しても、鍵は保存時に暗号化されたまま保たれます。Amber の起動時とロック解除時にパスフレーズの入力を求められます。 鍵を保護 - Amber を専用ユーザーで実行する - Amber は OS ユーザー「%1$s」を作成し、常にそのユーザーで実行します。これにより、このコンピューター上の他のアプリはメモリやファイルを読み取れなくなります。既存の Amber データはそのユーザーのホームに移動されます。設定するにはパスワード(sudo)を入力してください。 - あなたのパスワード(sudo) - 設定して再起動 - セットアップに失敗しました。パスワードを確認してもう一度お試しください パスフレーズを設定 今すぐロック パスフレーズを変更 diff --git a/desktop/src/main/resources/i18n/strings_ko.xml b/desktop/src/main/resources/i18n/strings_ko.xml index 8456a9c0..e56564ae 100644 --- a/desktop/src/main/resources/i18n/strings_ko.xml +++ b/desktop/src/main/resources/i18n/strings_ko.xml @@ -791,11 +791,6 @@ 로그아웃하려면 암호를 입력하세요 Amber에는 암호가 필요합니다. 파일을 읽을 수 있는 소프트웨어로부터도 키는 저장 시 암호화된 상태로 유지됩니다. Amber를 시작할 때와 잠금 해제 시 암호를 입력해야 합니다. 키 보호 - Amber를 전용 사용자로 실행 - Amber는 OS 사용자 “%1$s”을(를) 만들고 항상 해당 사용자로 실행되어, 이 컴퓨터의 다른 앱이 메모리나 파일을 읽을 수 없게 합니다. 기존 Amber 데이터는 해당 사용자의 홈으로 이동됩니다. 설정하려면 비밀번호(sudo)를 입력하세요. - 비밀번호(sudo) - 설정 후 다시 시작 - 설정에 실패했습니다. 비밀번호를 확인하고 다시 시도하세요 암호 설정 지금 잠그기 암호 변경 diff --git a/desktop/src/main/resources/i18n/strings_pt-BR.xml b/desktop/src/main/resources/i18n/strings_pt-BR.xml index 16b684eb..64738cf8 100644 --- a/desktop/src/main/resources/i18n/strings_pt-BR.xml +++ b/desktop/src/main/resources/i18n/strings_pt-BR.xml @@ -786,11 +786,6 @@ Digite sua senha para sair O Amber exige uma senha. Suas chaves permanecem criptografadas em repouso, mesmo contra softwares que possam ler seus arquivos — ela será solicitada quando o Amber iniciar e sempre que ele for bloqueado. Proteja suas chaves - Executar o Amber com um usuário próprio - O Amber criará o usuário do sistema “%1$s” e sempre executará sob ele, para que outros aplicativos neste computador não possam ler a memória nem os arquivos dele. Seus dados existentes do Amber são movidos para a pasta pessoal desse usuário. Digite sua senha (sudo) para configurar. - Sua senha (sudo) - Configurar e reiniciar - Falha na configuração — verifique sua senha e tente novamente Definir uma senha Bloquear agora Alterar senha diff --git a/desktop/src/main/resources/i18n/strings_ru.xml b/desktop/src/main/resources/i18n/strings_ru.xml index 4e3cc214..381bd3de 100644 --- a/desktop/src/main/resources/i18n/strings_ru.xml +++ b/desktop/src/main/resources/i18n/strings_ru.xml @@ -791,11 +791,6 @@ Введите пароль-фразу, чтобы выйти Amber требует пароль-фразу. Ваши ключи остаются зашифрованными на диске, даже от программ, способных читать ваши файлы, — она запрашивается при запуске Amber и после каждой блокировки. Защитите свои ключи - Запускать Amber под отдельным пользователем - Amber создаст системного пользователя «%1$s» и всегда будет работать под ним, чтобы другие приложения этого компьютера не могли читать его память и файлы. Существующие данные Amber переносятся в домашний каталог этого пользователя. Введите свой пароль (sudo) для настройки. - Ваш пароль (sudo) - Настроить и перезапустить - Не удалось настроить — проверьте пароль и попробуйте снова Задать пароль-фразу Заблокировать сейчас Изменить пароль-фразу diff --git a/desktop/src/main/resources/i18n/strings_th.xml b/desktop/src/main/resources/i18n/strings_th.xml index 4e60e1d2..e210e831 100644 --- a/desktop/src/main/resources/i18n/strings_th.xml +++ b/desktop/src/main/resources/i18n/strings_th.xml @@ -767,11 +767,6 @@ ป้อนวลีรหัสผ่านเพื่อออกจากระบบ Amber กำหนดให้ต้องมีวลีรหัสผ่าน กุญแจของคุณจะถูกเข้ารหัสไว้ขณะจัดเก็บ แม้กระทั่งจากซอฟต์แวร์ที่อ่านไฟล์ของคุณได้ และคุณจะถูกถามวลีรหัสผ่านเมื่อ Amber เริ่มทำงานและเมื่อถูกล็อก ปกป้องกุญแจของคุณ - เรียกใช้ Amber ภายใต้ผู้ใช้ของตัวเอง - Amber จะสร้างผู้ใช้ระบบ “%1$s” และทำงานภายใต้ผู้ใช้นั้นเสมอ เพื่อให้แอปอื่นในคอมพิวเตอร์เครื่องนี้อ่านหน่วยความจำหรือไฟล์ของมันไม่ได้ ข้อมูล Amber ที่มีอยู่ของคุณจะถูกย้ายไปที่โฮมของผู้ใช้นั้น ป้อนรหัสผ่านของคุณ (sudo) เพื่อตั้งค่า - รหัสผ่านของคุณ (sudo) - ตั้งค่าและรีสตาร์ท - การตั้งค่าล้มเหลว — ตรวจสอบรหัสผ่านแล้วลองอีกครั้ง ตั้งวลีรหัสผ่าน ล็อกทันที เปลี่ยนวลีรหัสผ่าน diff --git a/desktop/src/main/resources/i18n/strings_tr.xml b/desktop/src/main/resources/i18n/strings_tr.xml index 215cff7d..1883538a 100644 --- a/desktop/src/main/resources/i18n/strings_tr.xml +++ b/desktop/src/main/resources/i18n/strings_tr.xml @@ -787,11 +787,6 @@ Çıkış yapmak için parolanızı girin Amber bir parola gerektirir. Anahtarlarınız, dosyalarınızı okuyabilen yazılımlara karşı bile diskte şifreli kalır; parola Amber başlatıldığında ve her kilitlenmeden sonra istenir. Anahtarlarınızı koruyun - Amber'i kendi kullanıcısı altında çalıştır - Amber, “%1$s” işletim sistemi kullanıcısını oluşturacak ve her zaman onun altında çalışacak; böylece bu bilgisayardaki diğer uygulamalar belleğini veya dosyalarını okuyamayacak. Mevcut Amber verileriniz o kullanıcının ev dizinine taşınır. Kurulum için parolanızı (sudo) girin. - Parolanız (sudo) - Kur ve yeniden başlat - Kurulum başarısız oldu — parolanızı kontrol edip tekrar deneyin Parola belirle Şimdi kilitle Parolayı değiştir diff --git a/desktop/src/main/resources/i18n/strings_vi.xml b/desktop/src/main/resources/i18n/strings_vi.xml index fcac99de..281a6fdd 100644 --- a/desktop/src/main/resources/i18n/strings_vi.xml +++ b/desktop/src/main/resources/i18n/strings_vi.xml @@ -767,11 +767,6 @@ Nhập cụm mật khẩu để đăng xuất Amber yêu cầu có cụm mật khẩu. Khóa của bạn luôn được mã hóa khi lưu, ngay cả trước phần mềm có thể đọc tệp của bạn — bạn sẽ được yêu cầu nhập khi Amber khởi động và mỗi khi bị khóa. Bảo vệ khóa của bạn - Chạy Amber dưới người dùng riêng - Amber sẽ tạo người dùng hệ thống “%1$s” và luôn chạy dưới người dùng đó, để các ứng dụng khác trên máy tính này không thể đọc bộ nhớ hay tệp của nó. Dữ liệu Amber hiện có của bạn được chuyển sang thư mục chính của người dùng đó. Nhập mật khẩu của bạn (sudo) để thiết lập. - Mật khẩu của bạn (sudo) - Thiết lập và khởi động lại - Thiết lập thất bại — kiểm tra mật khẩu và thử lại Đặt cụm mật khẩu Khóa ngay Đổi cụm mật khẩu diff --git a/desktop/src/main/resources/i18n/strings_zh.xml b/desktop/src/main/resources/i18n/strings_zh.xml index a30970b6..cf65328c 100644 --- a/desktop/src/main/resources/i18n/strings_zh.xml +++ b/desktop/src/main/resources/i18n/strings_zh.xml @@ -772,11 +772,6 @@ 输入密码短语以退出登录 Amber 必须设置密码短语。即使面对能读取你文件的软件,密钥在存储时也保持加密——Amber 启动时和每次锁定后都会要求输入。 保护你的密钥 - 以专属用户运行 Amber - Amber 将创建系统用户“%1$s”并始终以该用户运行,这样这台电脑上的其他应用无法读取它的内存或文件。你现有的 Amber 数据会移动到该用户的主目录。输入你的密码(sudo)以完成设置。 - 你的密码(sudo) - 设置并重启 - 设置失败——请检查密码后重试 设置密码短语 立即锁定 更改密码短语 diff --git a/desktop/src/test/kotlin/com/greenart7c3/nostrsigner/desktop/DedicatedUserTest.kt b/desktop/src/test/kotlin/com/greenart7c3/nostrsigner/desktop/DedicatedUserTest.kt deleted file mode 100644 index 94f7077b..00000000 --- a/desktop/src/test/kotlin/com/greenart7c3/nostrsigner/desktop/DedicatedUserTest.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.greenart7c3.nostrsigner.desktop - -import com.greenart7c3.nostrsigner.desktop.core.DedicatedUser -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertThrows -import org.junit.Assert.assertTrue -import org.junit.Test - -/** - * Covers the pure generators behind the dedicated-user setup: the generated - * launcher must exec exactly one fixed binary (no argument pass-through, so - * the sudoers rule cannot be reused to run anything else), and the root - * script must create the user, migrate data, and validate the sudoers file. - */ -class DedicatedUserTest { - private val dollar = "$" - - @Test - fun userNameValidation() { - assertTrue(DedicatedUser.isValidUserName("amber")) - assertTrue(DedicatedUser.isValidUserName("am-ber_1")) - assertFalse(DedicatedUser.isValidUserName("")) - assertFalse(DedicatedUser.isValidUserName("Amber")) - assertFalse(DedicatedUser.isValidUserName("9amber")) - assertFalse(DedicatedUser.isValidUserName("amber;rm -rf /")) - assertFalse(DedicatedUser.isValidUserName("a".repeat(33))) - } - - @Test - fun launcherExecsExactlyOneFixedBinary() { - val script = DedicatedUser.wrapperScript( - exePath = "/opt/Amber/bin/Amber", - home = "/home/amber", - name = "amber", - ) - assertTrue(script.startsWith("#!/bin/sh")) - assertTrue(script.contains("exec '/opt/Amber/bin/Amber'\n")) - assertTrue(script.contains("HOME='/home/amber'")) - assertTrue(script.contains("USER='amber'")) - // Session socket locations arrive as positional arguments... - assertTrue(script.contains("XDG_RUNTIME_DIR='" + dollar + "1'")) - assertTrue(script.contains("DBUS_SESSION_BUS_ADDRESS='" + dollar + "4'")) - // ...and nothing else is forwarded: no argument or env pass-through. - assertFalse(script.contains("\"" + dollar + "@\"")) - assertFalse(script.contains("JDK_JAVA_OPTIONS")) - } - - @Test - fun sudoersRuleIsNarrow() { - val rule = DedicatedUser.sudoersRule( - invoker = "admin", - name = "amber", - wrapperPath = "/usr/local/bin/amber-runas-amber", - ) - assertEquals("admin ALL=(amber) NOPASSWD: /usr/local/bin/amber-runas-amber\n", rule) - } - - @Test - fun rootScriptCreatesUserMigratesDataAndValidates() { - val script = DedicatedUser.rootSetupScript( - name = "amber", - currentUserName = "admin", - exePath = "/opt/Amber/bin/Amber", - wrapperPath = "/usr/local/bin/amber-runas-amber", - sudoersPath = "/etc/sudoers.d/amber-runas-amber", - dataDir = "/home/admin/.local/share/amber", - amberDataDir = "/home/amber/.local/share/amber", - xdgRuntimeDir = "/run/user/1000", - waylandDisplay = "wayland-1", - x11Socket = "/run/user/1000/.X11-unix/X0", - ) - // User creation is guarded and idempotent. - assertTrue(script.contains("if ! id -u 'amber' >/dev/null 2>&1; then")) - assertTrue(script.contains("useradd --create-home")) - // Existing data of the invoking user is migrated once, with ownership. - assertTrue(script.contains("if [ -d '/home/admin/.local/share/amber' ] && [ ! -e '/home/amber/.local/share/amber' ]; then")) - assertTrue(script.contains("cp -a '/home/admin/.local/share/amber' '/home/amber/.local/share/amber'")) - assertTrue(script.contains("chown -R \"amber:\" '/home/amber/.local/share/amber'")) - // Launcher + sudoers payloads go in via quoted heredocs (no expansion). - assertTrue(script.contains("cat > '/usr/local/bin/amber-runas-amber' <<'WRAPPER_EOF'")) - assertTrue(script.contains("<<'SUDOERS_EOF'")) - assertTrue(script.contains("admin ALL=(amber) NOPASSWD: /usr/local/bin/amber-runas-amber\n")) - // The sudoers file is syntax-checked before it is installed. - assertTrue(script.contains("visudo -cf")) - assertTrue(script.contains("chmod 0440")) - // Heredoc terminators must be flush-left: an indented terminator makes - // the heredoc swallow the rest of the script. - val lines = script.lines() - assertTrue(lines.contains("WRAPPER_EOF")) - assertTrue(lines.contains("SUDOERS_EOF")) - // The launcher is embedded verbatim inside the first heredoc. - assertTrue(lines.contains("exec '/opt/Amber/bin/Amber'")) - assertTrue(lines.indexOf("exec '/opt/Amber/bin/Amber'") < lines.indexOf("WRAPPER_EOF")) - // Session sockets (Wayland, X11, D-Bus) are granted via ACLs. - assertTrue(script.contains("setfacl -m 'u:amber:rw' '/run/user/1000/wayland-1'")) - assertTrue(script.contains("setfacl -m 'u:amber:rw' '/run/user/1000/.X11-unix/X0'")) - assertTrue(script.contains("setfacl -m 'u:amber:rw' '/run/user/1000/bus'")) - } - - @Test - fun rootScriptRejectsInvalidUserName() { - assertThrows(IllegalArgumentException::class.java) { - DedicatedUser.rootSetupScript( - name = "amber; rm -rf /", - currentUserName = "admin", - exePath = "/opt/Amber/bin/Amber", - wrapperPath = "/usr/local/bin/amber-runas-amber", - sudoersPath = "/etc/sudoers.d/amber-runas-amber", - dataDir = "/home/admin/.local/share/amber", - amberDataDir = "/home/amber/.local/share/amber", - xdgRuntimeDir = "/run/user/1000", - waylandDisplay = "wayland-1", - x11Socket = null, - ) - } - } - - @Test - fun x11SocketPathDerivation() { - assertEquals("/run/user/1000/.X11-unix/X0", DedicatedUser.x11SocketPath(":0", "/run/user/1000")) - assertEquals("/run/user/1000/.X11-unix/X1", DedicatedUser.x11SocketPath(":1.0", "/run/user/1000")) - assertNull(DedicatedUser.x11SocketPath(null, "/run/user/1000")) - assertNull(DedicatedUser.x11SocketPath("", "/run/user/1000")) - assertNull(DedicatedUser.x11SocketPath("wayland-1", "/run/user/1000")) - } - - @Test - fun amberDataDirLivesInTheDedicatedHome() { - assertEquals("/home/amber/.local/share/amber", DedicatedUser.amberDataDir("amber")) - } - - @Test - fun relaunchCommandSwitchesToTheDedicatedUser() { - val command = DedicatedUser.relaunchCommand("/usr/local/bin/amber-runas-amber") - assertEquals("sudo", command[0]) - assertEquals("-n", command[1]) - assertEquals("-u", command[2]) - assertEquals(DedicatedUser.userName, command[3]) - assertEquals("/usr/local/bin/amber-runas-amber", command[4]) - // Followed by the four session socket arguments. - assertEquals(9, command.size) - } -}