mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-06 03:18:23 +00:00
desktop: run as the current user again
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.
This commit is contained in:
+1
-34
@@ -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-<name>`) 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-<name>`,
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>) : 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<String>): 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<String> = 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<String> = 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String?>(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
|
||||
}
|
||||
@@ -788,11 +788,6 @@
|
||||
<string name="d_log_out_passphrase">Gib deine Passphrase ein, um dich abzumelden</string>
|
||||
<string name="d_passphrase_desc">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.</string>
|
||||
<string name="d_setup_title">Schütze deine Schlüssel</string>
|
||||
<string name="d_dedicated_user_title">Amber unter einem eigenen Benutzer ausführen</string>
|
||||
<string name="d_dedicated_user_desc">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.</string>
|
||||
<string name="d_dedicated_user_password">Dein Passwort (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">Einrichten und neu starten</string>
|
||||
<string name="d_dedicated_user_failed">Einrichtung fehlgeschlagen – prüfe dein Passwort und versuche es erneut</string>
|
||||
<string name="d_set_passphrase">Passphrase festlegen</string>
|
||||
<string name="d_lock_now">Jetzt sperren</string>
|
||||
<string name="d_change_passphrase">Passphrase ändern</string>
|
||||
|
||||
@@ -799,11 +799,6 @@
|
||||
<string name="d_log_out_passphrase">Enter your passphrase to log out</string>
|
||||
<string name="d_passphrase_desc">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.</string>
|
||||
<string name="d_setup_title">Protect your keys</string>
|
||||
<string name="d_dedicated_user_title">Run Amber under its own user</string>
|
||||
<string name="d_dedicated_user_desc">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.</string>
|
||||
<string name="d_dedicated_user_password">Your password (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">Set up and restart</string>
|
||||
<string name="d_dedicated_user_failed">Setup failed — check your password and try again</string>
|
||||
<string name="d_set_passphrase">Set a passphrase</string>
|
||||
<string name="d_lock_now">Lock now</string>
|
||||
<string name="d_change_passphrase">Change passphrase</string>
|
||||
|
||||
@@ -791,11 +791,6 @@
|
||||
<string name="d_log_out_passphrase">Introduce tu frase de contraseña para cerrar sesión</string>
|
||||
<string name="d_passphrase_desc">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.</string>
|
||||
<string name="d_setup_title">Protege tus claves</string>
|
||||
<string name="d_dedicated_user_title">Ejecutar Amber con su propio usuario</string>
|
||||
<string name="d_dedicated_user_desc">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.</string>
|
||||
<string name="d_dedicated_user_password">Tu contraseña (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">Configurar y reiniciar</string>
|
||||
<string name="d_dedicated_user_failed">La configuración falló; comprueba tu contraseña e inténtalo de nuevo</string>
|
||||
<string name="d_set_passphrase">Establecer una frase de contraseña</string>
|
||||
<string name="d_lock_now">Bloquear ahora</string>
|
||||
<string name="d_change_passphrase">Cambiar frase de contraseña</string>
|
||||
|
||||
@@ -788,11 +788,6 @@
|
||||
<string name="d_log_out_passphrase">Saisissez votre phrase secrète pour vous déconnecter</string>
|
||||
<string name="d_passphrase_desc">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.</string>
|
||||
<string name="d_setup_title">Protégez vos clés</string>
|
||||
<string name="d_dedicated_user_title">Exécuter Amber sous son propre utilisateur</string>
|
||||
<string name="d_dedicated_user_desc">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.</string>
|
||||
<string name="d_dedicated_user_password">Votre mot de passe (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">Configurer et redémarrer</string>
|
||||
<string name="d_dedicated_user_failed">Échec de la configuration — vérifiez votre mot de passe et réessayez</string>
|
||||
<string name="d_set_passphrase">Définir une phrase secrète</string>
|
||||
<string name="d_lock_now">Verrouiller maintenant</string>
|
||||
<string name="d_change_passphrase">Changer la phrase secrète</string>
|
||||
|
||||
@@ -791,11 +791,6 @@
|
||||
<string name="d_log_out_passphrase">Masukkan frasa sandi Anda untuk keluar</string>
|
||||
<string name="d_passphrase_desc">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.</string>
|
||||
<string name="d_setup_title">Lindungi kunci Anda</string>
|
||||
<string name="d_dedicated_user_title">Jalankan Amber dengan penggunanya sendiri</string>
|
||||
<string name="d_dedicated_user_desc">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.</string>
|
||||
<string name="d_dedicated_user_password">Kata sandi Anda (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">Siapkan dan mulai ulang</string>
|
||||
<string name="d_dedicated_user_failed">Penyiapan gagal — periksa kata sandi Anda dan coba lagi</string>
|
||||
<string name="d_set_passphrase">Tetapkan frasa sandi</string>
|
||||
<string name="d_lock_now">Kunci sekarang</string>
|
||||
<string name="d_change_passphrase">Ubah frasa sandi</string>
|
||||
|
||||
@@ -791,11 +791,6 @@
|
||||
<string name="d_log_out_passphrase">Inserisci la tua passphrase per uscire</string>
|
||||
<string name="d_passphrase_desc">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.</string>
|
||||
<string name="d_setup_title">Proteggi le tue chiavi</string>
|
||||
<string name="d_dedicated_user_title">Esegui Amber con un utente dedicato</string>
|
||||
<string name="d_dedicated_user_desc">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.</string>
|
||||
<string name="d_dedicated_user_password">La tua password (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">Configura e riavvia</string>
|
||||
<string name="d_dedicated_user_failed">Configurazione non riuscita — controlla la password e riprova</string>
|
||||
<string name="d_set_passphrase">Imposta una passphrase</string>
|
||||
<string name="d_lock_now">Blocca ora</string>
|
||||
<string name="d_change_passphrase">Cambia passphrase</string>
|
||||
|
||||
@@ -767,11 +767,6 @@
|
||||
<string name="d_log_out_passphrase">ログアウトするにはパスフレーズを入力してください</string>
|
||||
<string name="d_passphrase_desc">Amber ではパスフレーズが必須です。ファイルを読み取れるソフトウェアに対しても、鍵は保存時に暗号化されたまま保たれます。Amber の起動時とロック解除時にパスフレーズの入力を求められます。</string>
|
||||
<string name="d_setup_title">鍵を保護</string>
|
||||
<string name="d_dedicated_user_title">Amber を専用ユーザーで実行する</string>
|
||||
<string name="d_dedicated_user_desc">Amber は OS ユーザー「%1$s」を作成し、常にそのユーザーで実行します。これにより、このコンピューター上の他のアプリはメモリやファイルを読み取れなくなります。既存の Amber データはそのユーザーのホームに移動されます。設定するにはパスワード(sudo)を入力してください。</string>
|
||||
<string name="d_dedicated_user_password">あなたのパスワード(sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">設定して再起動</string>
|
||||
<string name="d_dedicated_user_failed">セットアップに失敗しました。パスワードを確認してもう一度お試しください</string>
|
||||
<string name="d_set_passphrase">パスフレーズを設定</string>
|
||||
<string name="d_lock_now">今すぐロック</string>
|
||||
<string name="d_change_passphrase">パスフレーズを変更</string>
|
||||
|
||||
@@ -791,11 +791,6 @@
|
||||
<string name="d_log_out_passphrase">로그아웃하려면 암호를 입력하세요</string>
|
||||
<string name="d_passphrase_desc">Amber에는 암호가 필요합니다. 파일을 읽을 수 있는 소프트웨어로부터도 키는 저장 시 암호화된 상태로 유지됩니다. Amber를 시작할 때와 잠금 해제 시 암호를 입력해야 합니다.</string>
|
||||
<string name="d_setup_title">키 보호</string>
|
||||
<string name="d_dedicated_user_title">Amber를 전용 사용자로 실행</string>
|
||||
<string name="d_dedicated_user_desc">Amber는 OS 사용자 “%1$s”을(를) 만들고 항상 해당 사용자로 실행되어, 이 컴퓨터의 다른 앱이 메모리나 파일을 읽을 수 없게 합니다. 기존 Amber 데이터는 해당 사용자의 홈으로 이동됩니다. 설정하려면 비밀번호(sudo)를 입력하세요.</string>
|
||||
<string name="d_dedicated_user_password">비밀번호(sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">설정 후 다시 시작</string>
|
||||
<string name="d_dedicated_user_failed">설정에 실패했습니다. 비밀번호를 확인하고 다시 시도하세요</string>
|
||||
<string name="d_set_passphrase">암호 설정</string>
|
||||
<string name="d_lock_now">지금 잠그기</string>
|
||||
<string name="d_change_passphrase">암호 변경</string>
|
||||
|
||||
@@ -786,11 +786,6 @@
|
||||
<string name="d_log_out_passphrase">Digite sua senha para sair</string>
|
||||
<string name="d_passphrase_desc">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.</string>
|
||||
<string name="d_setup_title">Proteja suas chaves</string>
|
||||
<string name="d_dedicated_user_title">Executar o Amber com um usuário próprio</string>
|
||||
<string name="d_dedicated_user_desc">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.</string>
|
||||
<string name="d_dedicated_user_password">Sua senha (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">Configurar e reiniciar</string>
|
||||
<string name="d_dedicated_user_failed">Falha na configuração — verifique sua senha e tente novamente</string>
|
||||
<string name="d_set_passphrase">Definir uma senha</string>
|
||||
<string name="d_lock_now">Bloquear agora</string>
|
||||
<string name="d_change_passphrase">Alterar senha</string>
|
||||
|
||||
@@ -791,11 +791,6 @@
|
||||
<string name="d_log_out_passphrase">Введите пароль-фразу, чтобы выйти</string>
|
||||
<string name="d_passphrase_desc">Amber требует пароль-фразу. Ваши ключи остаются зашифрованными на диске, даже от программ, способных читать ваши файлы, — она запрашивается при запуске Amber и после каждой блокировки.</string>
|
||||
<string name="d_setup_title">Защитите свои ключи</string>
|
||||
<string name="d_dedicated_user_title">Запускать Amber под отдельным пользователем</string>
|
||||
<string name="d_dedicated_user_desc">Amber создаст системного пользователя «%1$s» и всегда будет работать под ним, чтобы другие приложения этого компьютера не могли читать его память и файлы. Существующие данные Amber переносятся в домашний каталог этого пользователя. Введите свой пароль (sudo) для настройки.</string>
|
||||
<string name="d_dedicated_user_password">Ваш пароль (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">Настроить и перезапустить</string>
|
||||
<string name="d_dedicated_user_failed">Не удалось настроить — проверьте пароль и попробуйте снова</string>
|
||||
<string name="d_set_passphrase">Задать пароль-фразу</string>
|
||||
<string name="d_lock_now">Заблокировать сейчас</string>
|
||||
<string name="d_change_passphrase">Изменить пароль-фразу</string>
|
||||
|
||||
@@ -767,11 +767,6 @@
|
||||
<string name="d_log_out_passphrase">ป้อนวลีรหัสผ่านเพื่อออกจากระบบ</string>
|
||||
<string name="d_passphrase_desc">Amber กำหนดให้ต้องมีวลีรหัสผ่าน กุญแจของคุณจะถูกเข้ารหัสไว้ขณะจัดเก็บ แม้กระทั่งจากซอฟต์แวร์ที่อ่านไฟล์ของคุณได้ และคุณจะถูกถามวลีรหัสผ่านเมื่อ Amber เริ่มทำงานและเมื่อถูกล็อก</string>
|
||||
<string name="d_setup_title">ปกป้องกุญแจของคุณ</string>
|
||||
<string name="d_dedicated_user_title">เรียกใช้ Amber ภายใต้ผู้ใช้ของตัวเอง</string>
|
||||
<string name="d_dedicated_user_desc">Amber จะสร้างผู้ใช้ระบบ “%1$s” และทำงานภายใต้ผู้ใช้นั้นเสมอ เพื่อให้แอปอื่นในคอมพิวเตอร์เครื่องนี้อ่านหน่วยความจำหรือไฟล์ของมันไม่ได้ ข้อมูล Amber ที่มีอยู่ของคุณจะถูกย้ายไปที่โฮมของผู้ใช้นั้น ป้อนรหัสผ่านของคุณ (sudo) เพื่อตั้งค่า</string>
|
||||
<string name="d_dedicated_user_password">รหัสผ่านของคุณ (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">ตั้งค่าและรีสตาร์ท</string>
|
||||
<string name="d_dedicated_user_failed">การตั้งค่าล้มเหลว — ตรวจสอบรหัสผ่านแล้วลองอีกครั้ง</string>
|
||||
<string name="d_set_passphrase">ตั้งวลีรหัสผ่าน</string>
|
||||
<string name="d_lock_now">ล็อกทันที</string>
|
||||
<string name="d_change_passphrase">เปลี่ยนวลีรหัสผ่าน</string>
|
||||
|
||||
@@ -787,11 +787,6 @@
|
||||
<string name="d_log_out_passphrase">Çıkış yapmak için parolanızı girin</string>
|
||||
<string name="d_passphrase_desc">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.</string>
|
||||
<string name="d_setup_title">Anahtarlarınızı koruyun</string>
|
||||
<string name="d_dedicated_user_title">Amber'i kendi kullanıcısı altında çalıştır</string>
|
||||
<string name="d_dedicated_user_desc">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.</string>
|
||||
<string name="d_dedicated_user_password">Parolanız (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">Kur ve yeniden başlat</string>
|
||||
<string name="d_dedicated_user_failed">Kurulum başarısız oldu — parolanızı kontrol edip tekrar deneyin</string>
|
||||
<string name="d_set_passphrase">Parola belirle</string>
|
||||
<string name="d_lock_now">Şimdi kilitle</string>
|
||||
<string name="d_change_passphrase">Parolayı değiştir</string>
|
||||
|
||||
@@ -767,11 +767,6 @@
|
||||
<string name="d_log_out_passphrase">Nhập cụm mật khẩu để đăng xuất</string>
|
||||
<string name="d_passphrase_desc">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.</string>
|
||||
<string name="d_setup_title">Bảo vệ khóa của bạn</string>
|
||||
<string name="d_dedicated_user_title">Chạy Amber dưới người dùng riêng</string>
|
||||
<string name="d_dedicated_user_desc">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.</string>
|
||||
<string name="d_dedicated_user_password">Mật khẩu của bạn (sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">Thiết lập và khởi động lại</string>
|
||||
<string name="d_dedicated_user_failed">Thiết lập thất bại — kiểm tra mật khẩu và thử lại</string>
|
||||
<string name="d_set_passphrase">Đặt cụm mật khẩu</string>
|
||||
<string name="d_lock_now">Khóa ngay</string>
|
||||
<string name="d_change_passphrase">Đổi cụm mật khẩu</string>
|
||||
|
||||
@@ -772,11 +772,6 @@
|
||||
<string name="d_log_out_passphrase">输入密码短语以退出登录</string>
|
||||
<string name="d_passphrase_desc">Amber 必须设置密码短语。即使面对能读取你文件的软件,密钥在存储时也保持加密——Amber 启动时和每次锁定后都会要求输入。</string>
|
||||
<string name="d_setup_title">保护你的密钥</string>
|
||||
<string name="d_dedicated_user_title">以专属用户运行 Amber</string>
|
||||
<string name="d_dedicated_user_desc">Amber 将创建系统用户“%1$s”并始终以该用户运行,这样这台电脑上的其他应用无法读取它的内存或文件。你现有的 Amber 数据会移动到该用户的主目录。输入你的密码(sudo)以完成设置。</string>
|
||||
<string name="d_dedicated_user_password">你的密码(sudo)</string>
|
||||
<string name="d_dedicated_user_set_up">设置并重启</string>
|
||||
<string name="d_dedicated_user_failed">设置失败——请检查密码后重试</string>
|
||||
<string name="d_set_passphrase">设置密码短语</string>
|
||||
<string name="d_lock_now">立即锁定</string>
|
||||
<string name="d_change_passphrase">更改密码短语</string>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user