From a818a83a9c6b8d8a84b2b2e251ff09273f85b627 Mon Sep 17 00:00:00 2001 From: samialabri Date: Sat, 31 Oct 2026 08:24:59 +0400 Subject: [PATCH] Local vaults without a server; Snap package; version 1.2.2 The New vault tab can now skip the server, as Apple and Android already do. The snap writes its browser manifests on start, since a strict snap can't install them itself. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 4 +- package.json | 2 +- scripts/snap.sh | 15 ++++++++ snap/gui/kestrove.desktop | 9 +++++ snap/gui/kestrove.png | Bin 0 -> 9877 bytes snap/snapcraft.yaml | 62 ++++++++++++++++++++++++++++++ src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 1 + src-tauri/src/main.rs | 10 +++++ src-tauri/src/snap.rs | 79 ++++++++++++++++++++++++++++++++++++++ src-tauri/src/sync.rs | 24 ++++++++++++ src-tauri/tauri.conf.json | 2 +- src/main.ts | 25 +++++++++--- src/style.css | 1 + 15 files changed, 227 insertions(+), 11 deletions(-) create mode 100755 scripts/snap.sh create mode 100644 snap/gui/kestrove.desktop create mode 100644 snap/gui/kestrove.png create mode 100644 snap/snapcraft.yaml create mode 100644 src-tauri/src/snap.rs diff --git a/package-lock.json b/package-lock.json index 86a317d..25bf989 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kestrove-desktop", - "version": "1.2.1", + "version": "1.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kestrove-desktop", - "version": "1.2.1", + "version": "1.2.2", "license": "Apache-2.0", "dependencies": { "@tauri-apps/api": "2.12.3" diff --git a/package.json b/package.json index 5aa4cb2..460f0bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kestrove-desktop", - "version": "1.2.1", + "version": "1.2.2", "private": true, "license": "Apache-2.0", "type": "module", diff --git a/scripts/snap.sh b/scripts/snap.sh new file mode 100755 index 0000000..51e6f31 --- /dev/null +++ b/scripts/snap.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Builds the Snap Store package. Needs snapcraft and LXD. Run from anywhere; kestrove-core must sit next to this repo. +# The snap is built from a copy, because snapcraft only sees files under the project and the app needs both repos. +# +# scripts/snap.sh writes kestrove__amd64.snap to ../release/desktop +set -eu +cd "$(dirname "$0")/../.." +OUT=$(pwd)/release/desktop +WORK=$(mktemp -d "${TMPDIR:-$HOME}/kestrove-snap.XXXXXX") +trap 'rm -rf "$WORK"' EXIT +tar --exclude=node_modules --exclude=target --exclude=dist --exclude=.git -cf - kestrove-core kestrove-desktop | tar -xf - -C "$WORK" +cp -r kestrove-desktop/snap "$WORK/snap" +mkdir -p "$OUT" +(cd "$WORK" && snapcraft pack && cp kestrove_*.snap "$OUT/") +ls -la "$OUT"/kestrove_*.snap diff --git a/snap/gui/kestrove.desktop b/snap/gui/kestrove.desktop new file mode 100644 index 0000000..db9cd74 --- /dev/null +++ b/snap/gui/kestrove.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=Kestrove +Comment=Self-hosted, end-to-end encrypted password manager +Exec=kestrove %U +Icon=${SNAP}/meta/gui/kestrove.png +Categories=Utility;Security; +StartupWMClass=kestrove-desktop +Terminal=false diff --git a/snap/gui/kestrove.png b/snap/gui/kestrove.png new file mode 100644 index 0000000000000000000000000000000000000000..d10abd4bc24ae33134d7fe99e3d51c4841db294f GIT binary patch literal 9877 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 TgFS-)`8#4}V_I;)EB5~YcZhe5 literal 0 HcmV?d00001 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000..e6e6d79 --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,62 @@ +# Kestrove for the Snap Store. Build with scripts/snap.sh, which lays out kestrove-core next to kestrove-desktop. +name: kestrove +title: Kestrove +base: core24 +version: "1.2.2" +summary: Self-hosted, end-to-end encrypted password manager +description: | + Kestrove keeps your passwords, passkeys and one-time codes in a vault encrypted on your device. Sync it through a + server you run yourself, or keep it on one computer. The Kestrove browser extension fills logins through this app. +license: Apache-2.0 +website: https://kestrove.samialabri.com +grade: stable +confinement: strict + +plugs: + # Lets the app write the browser manifests (src-tauri/src/snap.rs). Needs store approval to connect automatically. + browser-native-hosts: + interface: personal-files + write: + - $HOME/.mozilla/native-messaging-hosts + - $HOME/.config/google-chrome/NativeMessagingHosts + - $HOME/.config/chromium/NativeMessagingHosts + - $HOME/.config/microsoft-edge/NativeMessagingHosts + - $HOME/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts + +apps: + kestrove: + command: usr/bin/kestrove-desktop + extensions: [gnome] + plugs: + - network + - password-manager-service + - browser-native-hosts + nmh: + command: usr/bin/kestrove-nmh + +parts: + kestrove: + plugin: nil + source: . + build-snaps: + - node/22/stable + build-packages: + - curl + - libayatana-appindicator3-dev + - librsvg2-dev + - libsecret-1-dev + - libssl-dev + - libxdo-dev + - patchelf + - pkg-config + stage-packages: + - libayatana-appindicator3-1 + - libxdo3 + override-build: | + curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal + . "$HOME/.cargo/env" + cd kestrove-desktop + npm ci --no-audit --no-fund + npm run tauri build -- --no-bundle + install -D src-tauri/target/release/kestrove-desktop "$CRAFT_PART_INSTALL/usr/bin/kestrove-desktop" + install -D src-tauri/target/release/kestrove-nmh "$CRAFT_PART_INSTALL/usr/bin/kestrove-nmh" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 775e91a..779b1f4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3064,7 +3064,7 @@ dependencies = [ [[package]] name = "kestrove-desktop" -version = "1.2.1" +version = "1.2.2" dependencies = [ "arboard", "base64 0.22.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e163bb9..23394b9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kestrove-desktop" -version = "1.2.1" +version = "1.2.2" edition = "2021" rust-version = "1.82" license = "Apache-2.0" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 89c807e..223b2d8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,7 @@ pub mod autolock; pub mod device_key; pub mod hello; pub mod qr; +pub mod snap; pub mod sync; pub mod title_bar; pub mod updates; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fecf2e5..a6da7f9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -232,6 +232,14 @@ async fn create_account( sk.map(|s| s.to_string()) } +/// A new vault on this computer only. +#[tauri::command] +async fn create_local_account(app: State<'_, App>, master_password: String) -> CmdResult { + let sk = app.sync.create_local_account(Zeroizing::new(master_password)); + app.activity.touch(); + sk.map(|s| s.to_string()) +} + /// `secret_key` may be omitted once this device remembers it. #[tauri::command] async fn unlock(app: State<'_, App>, master_password: String, secret_key: Option) -> CmdResult<()> { @@ -814,6 +822,7 @@ fn setup(app: &mut tauri::App) -> Result<(), Box> { } fn main() { + kestrove_desktop::snap::register_native_host(); let builder = tauri::Builder::default(); #[cfg(windows)] let builder = builder.plugin(tauri_plugin_updater::Builder::new().build()); @@ -830,6 +839,7 @@ fn main() { state, set_title_bar_colors, create_account, + create_local_account, unlock, hello_unlock, set_hello, diff --git a/src-tauri/src/snap.rs b/src-tauri/src/snap.rs new file mode 100644 index 0000000..34126e3 --- /dev/null +++ b/src-tauri/src/snap.rs @@ -0,0 +1,79 @@ +//! Browser registration for the Snap Store build. A snap can't ship files into the user's home, so the app writes +//! the native messaging manifests itself on startup, through the `browser-native-hosts` plug +//! (snap/snapcraft.yaml). Outside a snap this does nothing: the .deb and .rpm install them system-wide. + +use std::fs; +use std::path::Path; + +const HOST: &str = "app.kestrove.nmh"; +/// The helper's snap command. A browser, or the portal for snap Firefox, starts it inside the Kestrove snap, where +/// it shares the app's runtime directory and so its socket. +const NMH: &str = "/snap/bin/kestrove.nmh"; +const FIREFOX_IDS: &[&str] = &["browser@kestrove.app"]; +/// The Chrome Web Store build, then the unpacked development build. +const CHROME_ORIGINS: &[&str] = + &["chrome-extension://pmhfoafjoogobhfadaicklhkldgnafmk/", "chrome-extension://kogapfibnnpeapfcjkkpgjddbhnjfein/"]; +const FIREFOX_DIR: &str = ".mozilla/native-messaging-hosts"; +const CHROME_DIRS: &[&str] = &[ + ".config/google-chrome/NativeMessagingHosts", + ".config/chromium/NativeMessagingHosts", + ".config/microsoft-edge/NativeMessagingHosts", + ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts", +]; + +/// Writes the manifests when running as a snap. A browser that isn't installed, or a plug that isn't connected, only +/// means no browser integration for that browser, so failures are logged and the app carries on. +pub fn register_native_host() { + if std::env::var_os("SNAP").is_none() { + return; + } + let Some(home) = std::env::var_os("SNAP_REAL_HOME") else { + eprintln!("kestrove: SNAP_REAL_HOME is not set; browser integration not registered"); + return; + }; + for (dir, res) in write_manifests(Path::new(&home)) { + if let Err(e) = res { + eprintln!("kestrove: can't register the browser helper in {dir}: {e}"); + } + } +} + +fn write_manifests(home: &Path) -> Vec<(&'static str, std::io::Result<()>)> { + let manifest = |key: &str, ids: &[&str]| { + let doc = serde_json::json!({ + "name": HOST, "description": "Kestrove native messaging host", "path": NMH, "type": "stdio", key: ids, + }); + doc.to_string() + }; + let firefox = manifest("allowed_extensions", FIREFOX_IDS); + let chrome = manifest("allowed_origins", CHROME_ORIGINS); + let targets = std::iter::once((FIREFOX_DIR, &firefox)).chain(CHROME_DIRS.iter().map(|d| (*d, &chrome))); + targets.map(|(dir, body)| (dir, write(&home.join(dir), body))).collect() +} + +fn write(dir: &Path, body: &str) -> std::io::Result<()> { + fs::create_dir_all(dir)?; + fs::write(dir.join(format!("{HOST}.json")), body) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn writes_a_manifest_for_each_browser() { + let home = tempfile::tempdir().unwrap(); + assert!(write_manifests(home.path()).iter().all(|(_, r)| r.is_ok())); + + let read = |dir: &str| -> serde_json::Value { + let body = fs::read_to_string(home.path().join(dir).join(format!("{HOST}.json"))).unwrap(); + serde_json::from_str(&body).unwrap() + }; + let firefox = read(FIREFOX_DIR); + assert_eq!(firefox["path"], NMH); + assert_eq!(firefox["allowed_extensions"][0], FIREFOX_IDS[0]); + for dir in CHROME_DIRS { + assert_eq!(read(dir)["allowed_origins"].as_array().unwrap().len(), CHROME_ORIGINS.len()); + } + } +} diff --git a/src-tauri/src/sync.rs b/src-tauri/src/sync.rs index 632512c..bfe16b0 100644 --- a/src-tauri/src/sync.rs +++ b/src-tauri/src/sync.rs @@ -403,6 +403,17 @@ impl SyncService { Ok(sk) } + /// A vault on this computer only. Settings → Sync can connect it to a server later. Returns the Secret Key for + /// the Emergency Kit. + pub fn create_local_account(&self, master_password: Zeroizing) -> Result, String> { + if self.core.has_account() { + return Err("This computer already has a vault.".into()); + } + let sk = Zeroizing::new(self.core.create_account(master_password.to_string()).map_err(|e| describe(&e))?); + self.remember_secret_key(&sk); + Ok(sk) + } + /// For a vault created before sync was set up: registers it with the server (setup code, invite, or nothing /// with open sign-up). pub async fn register(&self, setup_code: &str, email: &str) -> Result<(), String> { @@ -668,6 +679,19 @@ mod tests { assert!(!device_name().is_empty()); } + #[tokio::test] + async fn a_local_vault_needs_no_server() { + let dir = tempfile::tempdir().unwrap(); + let core = VaultCore::new_in_memory(); + let key = DeviceKey::load_or_create_file(dir.path()).map_err(|e| e.to_string()); + let svc = SyncService::new(core.clone(), dir.path(), key, Box::new(|_| {})); + let sk = svc.create_local_account(Zeroizing::new("correct horse battery".into())).unwrap(); + assert!(core.has_account()); + assert_eq!(svc.status().server_url, None); + assert_eq!(svc.stored_secret_key().as_deref().map(String::as_str), Some(sk.as_str())); + assert!(svc.create_local_account(Zeroizing::new("correct horse battery".into())).is_err()); + } + #[tokio::test] async fn settings_persist_and_secrets_stay_out_of_views() { let dir = tempfile::tempdir().unwrap(); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f6b8f74..a091872 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Kestrove", - "version": "1.2.1", + "version": "1.2.2", "identifier": "app.kestrove.desktop", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/main.ts b/src/main.ts index db09ca6..0acb354 100644 --- a/src/main.ts +++ b/src/main.ts @@ -208,7 +208,7 @@ type Tab = "phone" | "kit" | "paste" | "create"; async function renderOnboarding(tab: Tab = "phone") { const status = await invoke("sync_status"); const body = stack(); - const tabs: [Tab, string][] = [["phone", "Use my phone"], ["kit", "Emergency Kit"], ["paste", "Paste setup code"], ["create", "New account"]]; + const tabs: [Tab, string][] = [["phone", "Use my phone"], ["kit", "Emergency Kit"], ["paste", "Paste setup code"], ["create", "New vault"]]; const nav = h("div", "seg", ...tabs.map(([t, label]) => { const b = button(label, "quiet", async () => { await invoke("join_cancel"); @@ -417,8 +417,17 @@ function createPanel(body: HTMLElement, status: SyncStatus) { const pins = input({ placeholder: "base64, comma-separated", spellcheck: false }, true); const pw = input({ type: "password", autocomplete: "new-password" }); const pw2 = input({ type: "password", autocomplete: "new-password" }); + const useServer = input({ type: "checkbox", checked: true }); + useServer.className = "switch"; + useServer.setAttribute("aria-label", "Sync with a server"); + const serverFields = stack(); const err = errBox(); const btn = button("Create account", "primary"); + const label = () => (useServer.checked ? "Create account" : "Create vault"); + useServer.onchange = () => { + serverFields.hidden = !useServer.checked; + btn.textContent = label(); + }; btn.onclick = async () => { err.replaceChildren(); if (pw.value !== pw2.value) { @@ -430,25 +439,31 @@ function createPanel(body: HTMLElement, status: SyncStatus) { btn.disabled = true; btn.textContent = "Creating…"; try { - const secretKey = await invoke("create_account", { serverUrl: url.value, pins: pins.value, setupCode: code.value, email: email.value, masterPassword: pw.value }); + const secretKey = useServer.checked + ? await invoke("create_account", { serverUrl: url.value, pins: pins.value, setupCode: code.value, email: email.value, masterPassword: pw.value }) + : await invoke("create_local_account", { masterPassword: pw.value }); pw.value = pw2.value = ""; await showSecretKey(secretKey); } catch (e) { setErr(err, e); btn.disabled = false; - btn.textContent = "Create account"; + btn.textContent = label(); // A vault created here whose server registration failed: continue to the vault (Settings → Sync retries). if ((await invoke("state")).has_account) { body.append(button("Continue to the vault", "secondary", render)); } } }; - body.append( - p("kv-muted", "A new account on your Kestrove server. You'll get a Secret Key: keep it with your Emergency Kit."), + serverFields.append( field("Your Kestrove server", url), field("Email", email, "You sign in with it on new devices."), field("Invite or setup code", code, "The invite from your server's admin, or KV_SETUP_CODE from its .env for the first account. Empty with open sign-up."), h("details", "", h("summary", "kv-small", "Certificate pins (advanced)"), field("SPKI pins", pins)), + ); + body.append( + p("kv-muted", "A new vault. You'll get a Secret Key: keep it with your Emergency Kit."), + field("Sync with a server", useServer, "Turn this off to use Kestrove on this computer only. You can connect a server later in Settings."), + serverFields, field("Master password", pw, "10 or more characters."), field("Repeat master password", pw2), h("div", "actions", btn), err, diff --git a/src/style.css b/src/style.css index a8fb30e..160f00e 100644 --- a/src/style.css +++ b/src/style.css @@ -11,6 +11,7 @@ h1, h2, h3 { margin: 0; } .grow { flex: 1 1 auto; min-width: 0; } .row { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; } .stack { display: flex; flex-direction: column; gap: var(--space-4); min-width: 0; } +.stack[hidden] { display: none; } .actions { display: flex; gap: var(--space-2); align-items: center; } .actions.end { justify-content: flex-end; } .center-row { display: flex; justify-content: center; }