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 <[email protected]>
This commit is contained in:
Generated
+1
-1
@@ -3064,7 +3064,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kestrove-desktop"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"base64 0.22.1",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String> {
|
||||
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<String>) -> CmdResult<()> {
|
||||
@@ -814,6 +822,7 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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] = &["[email protected]"];
|
||||
/// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>) -> Result<Zeroizing<String>, 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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user