Compare commits
4
Commits
d70dcc04b6
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a818a83a9c | ||
|
|
608d85c297 | ||
|
|
32eadc4d03 | ||
|
|
57b728c7e3 |
@@ -63,15 +63,21 @@ XWIN_ACCEPT_LICENSE=1 scripts/windows.sh
|
|||||||
`src-tauri/Cargo.toml`, tag the commit (`git tag v<version> && git push origin v<version>`), then:
|
`src-tauri/Cargo.toml`, tag the commit (`git tag v<version> && git push origin v<version>`), then:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
GITEA_TOKEN=<token with write:repository> TAURI_SIGNING_PRIVATE_KEY_PATH=<updater key> scripts/gitea-release.sh
|
GITEA_TOKEN=<token with write:repository> scripts/gitea-release.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
It creates the release for the tag and uploads the installers, the browser helpers and a `SHA256SUMS` file.
|
It creates the release for the tag and uploads the installers, the browser helpers and a `SHA256SUMS` file.
|
||||||
|
|
||||||
**Offer it as an update (Windows):** the script also signs the installer with the updater key, uploads the
|
**Put it on the website and offer it as an update (Windows):**
|
||||||
signature, and writes `updater/latest.json`. Sign the installer with the code-signing certificate first, because
|
|
||||||
the updater signature covers the final file. Installed copies are offered the update once that file is committed
|
```bash
|
||||||
and pushed to main; the script prints the commands. Keep the updater key and its password offline and backed up:
|
TAURI_SIGNING_PRIVATE_KEY_PATH=<updater key> scripts/publish-update.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This signs the installer with the updater key, copies it to `downloads/` in `kestrove-website` next to this repo,
|
||||||
|
and writes `updates/latest.json` there. Sign the installer with the code-signing certificate first, because the
|
||||||
|
updater signature covers the final file. Installed copies are offered the update once the website is deployed.
|
||||||
|
Keep the updater key and its password offline and backed up:
|
||||||
the app only accepts updates signed with it, so losing it means existing installs can't update.
|
the app only accepts updates signed with it, so losing it means existing installs can't update.
|
||||||
|
|
||||||
## How the browser connection works
|
## How the browser connection works
|
||||||
@@ -93,7 +99,7 @@ browser extension ─(native messaging)─▶ kestrove-nmh ─(local connection)
|
|||||||
## Updates
|
## Updates
|
||||||
|
|
||||||
On Windows, Kestrove checks for a new version a minute after it starts and once a day. The check is a plain request
|
On Windows, Kestrove checks for a new version a minute after it starts and once a day. The check is a plain request
|
||||||
for `updater/latest.json` on the Kestrove Gitea: it reveals this computer's IP address and app version, and nothing
|
for `updates/latest.json` on the Kestrove website: it reveals this computer's IP address and app version, and nothing
|
||||||
from your vault. A new version appears as a banner and installs only when you choose **Update and restart**. The app
|
from your vault. A new version appears as a banner and installs only when you choose **Update and restart**. The app
|
||||||
verifies the download's signature before running it. Turn checks off in **Settings → Updates**. On Linux, updates
|
verifies the download's signature before running it. Turn checks off in **Settings → Updates**. On Linux, updates
|
||||||
come through your package manager.
|
come through your package manager.
|
||||||
|
|||||||
@@ -27,5 +27,9 @@ native-messaging/install.sh --uninstall
|
|||||||
| Firefox | `~/.mozilla/native-messaging-hosts/app.kestrove.nmh.json` | `HKCU\Software\Mozilla\NativeMessagingHosts\app.kestrove.nmh` |
|
| Firefox | `~/.mozilla/native-messaging-hosts/app.kestrove.nmh.json` | `HKCU\Software\Mozilla\NativeMessagingHosts\app.kestrove.nmh` |
|
||||||
| Brave | `~/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts/` | same key as Chrome |
|
| Brave | `~/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts/` | same key as Chrome |
|
||||||
|
|
||||||
|
The Linux `.deb` and `.rpm` install these manifests system-wide (`src-tauri/linux/`), so they need no script. That
|
||||||
|
covers snap Firefox too: it can't start the host itself, so the desktop portal does, after asking the user once.
|
||||||
|
AppImage users run `install.sh`.
|
||||||
|
|
||||||
`allowed_origins` / `allowed_extensions` pin the host to **your** extension id.
|
`allowed_origins` / `allowed_extensions` pin the host to **your** extension id.
|
||||||
That is the first line of defense against other extensions (docs/04 A8).
|
That is the first line of defense against other extensions (docs/04 A8).
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ esac
|
|||||||
eval "d=\$dir_$b"
|
eval "d=\$dir_$b"
|
||||||
[ -d "$d" ] && BROWSERS="$BROWSERS $b"
|
[ -d "$d" ] && BROWSERS="$BROWSERS $b"
|
||||||
done
|
done
|
||||||
|
# Snap Firefox keeps its profile in ~/snap, but its portal reads the host from ~/.mozilla like any other Firefox.
|
||||||
|
case " $BROWSERS " in *" firefox "*) ;; *)
|
||||||
|
[ -n "$USER_DATA_DIR" ] || { [ -d /snap/firefox ] && BROWSERS="$BROWSERS firefox"; } || true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
render() { # template → stdout
|
render() { # template → stdout
|
||||||
sed -e "s|__NMH_PATH__|$NMH|" -e "s|chrome-extension://__CHROME_EXTENSION_ID__/|$CHROME_ORIGINS|" -e "s|__FIREFOX_EXTENSION_ID__|$FIREFOX_ID|" "$HERE/$HOST.$1.json"
|
sed -e "s|__NMH_PATH__|$NMH|" -e "s|chrome-extension://__CHROME_EXTENSION_ID__/|$CHROME_ORIGINS|" -e "s|__FIREFOX_EXTENSION_ID__|$FIREFOX_ID|" "$HERE/$HOST.$1.json"
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "kestrove-desktop",
|
"name": "kestrove-desktop",
|
||||||
"version": "1.2.0",
|
"version": "1.2.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "kestrove-desktop",
|
"name": "kestrove-desktop",
|
||||||
"version": "1.2.0",
|
"version": "1.2.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "2.12.3"
|
"@tauri-apps/api": "2.12.3"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kestrove-desktop",
|
"name": "kestrove-desktop",
|
||||||
"version": "1.2.0",
|
"version": "1.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Creates (or reuses) the Gitea release for this version, uploads the desktop bundles to it, and writes
|
# Creates (or reuses) the Gitea release for this version and uploads the desktop bundles to it. The website's
|
||||||
# updater/latest.json, which the Windows app checks for updates (src-tauri/src/updates.rs).
|
# downloads and the Windows app's updates come from scripts/publish-update.sh instead.
|
||||||
#
|
#
|
||||||
# GITEA_TOKEN=… TAURI_SIGNING_PRIVATE_KEY_PATH=… scripts/gitea-release.sh [dir-with-files] (default ../release/desktop)
|
# GITEA_TOKEN=… scripts/gitea-release.sh [dir-with-files] (default ../release/desktop)
|
||||||
#
|
#
|
||||||
# GITEA_TOKEN: a Gitea access token with the write:repository scope (Settings → Applications). The tag
|
# GITEA_TOKEN: a Gitea access token with the write:repository scope (Settings → Applications). The tag
|
||||||
# v<version> must already be pushed. Files: *.AppImage, *.deb, *.rpm, *-setup.exe, kestrove-nmh-*, SHA256SUMS.
|
# v<version> must already be pushed. Files: *.AppImage, *.deb, *.rpm, *-setup.exe, kestrove-nmh-*, SHA256SUMS.
|
||||||
#
|
|
||||||
# Updates: the Windows installer needs an updater signature (<installer>.sig) from the Kestrove updater key, whose
|
|
||||||
# public half is in src-tauri/tauri.conf.json. Sign after Authenticode signing: the signature covers the final
|
|
||||||
# file. With TAURI_SIGNING_PRIVATE_KEY_PATH set, a missing .sig is made here (the key's password is asked for, or
|
|
||||||
# read from TAURI_SIGNING_PRIVATE_KEY_PASSWORD). Optional NOTES: the text shown with the update.
|
|
||||||
# Users get the update only once updater/latest.json is committed and pushed to main (printed at the end).
|
|
||||||
set -eu
|
set -eu
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
: "${GITEA_TOKEN:?set GITEA_TOKEN (Gitea → Settings → Applications → token with write:repository)}"
|
: "${GITEA_TOKEN:?set GITEA_TOKEN (Gitea → Settings → Applications → token with write:repository)}"
|
||||||
@@ -21,17 +15,8 @@ DIR=${1:-../release/desktop}
|
|||||||
V=$(node -p 'require("./package.json").version')
|
V=$(node -p 'require("./package.json").version')
|
||||||
TAG="v$V"
|
TAG="v$V"
|
||||||
AUTH="Authorization: token $GITEA_TOKEN"
|
AUTH="Authorization: token $GITEA_TOKEN"
|
||||||
SETUP="$DIR/Kestrove_${V}_x64-setup.exe"
|
|
||||||
|
|
||||||
if [ -f "$SETUP" ] && [ ! -f "$SETUP.sig" ]; then
|
(cd "$DIR" && rm -f SHA256SUMS && for f in *.AppImage *.deb *.rpm *-setup.exe kestrove-nmh-*; do if [ -f "$f" ]; then shasum -a 256 -- "$f"; fi; done > SHA256SUMS; cat SHA256SUMS)
|
||||||
: "${TAURI_SIGNING_PRIVATE_KEY_PATH:?$SETUP.sig is missing: set TAURI_SIGNING_PRIVATE_KEY_PATH to the updater key to sign it}"
|
|
||||||
# --app-version binds the signature to this version (the app requires it), so an older signed installer can't
|
|
||||||
# be offered again under a newer version number.
|
|
||||||
npx tauri signer sign -f "$TAURI_SIGNING_PRIVATE_KEY_PATH" ${TAURI_SIGNING_PRIVATE_KEY_PASSWORD+-p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD"} \
|
|
||||||
--app-version "$V" "$SETUP"
|
|
||||||
fi
|
|
||||||
|
|
||||||
(cd "$DIR" && rm -f SHA256SUMS && shasum -a 256 -- *.AppImage *.deb *.rpm *-setup.exe kestrove-nmh-* 2>/dev/null > SHA256SUMS; cat SHA256SUMS)
|
|
||||||
|
|
||||||
ID=$(curl -fsS -H "$AUTH" "$API/repos/$REPO/releases/tags/$TAG" 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{console.log(JSON.parse(s).id||"")}catch{console.log("")}})')
|
ID=$(curl -fsS -H "$AUTH" "$API/repos/$REPO/releases/tags/$TAG" 2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{console.log(JSON.parse(s).id||"")}catch{console.log("")}})')
|
||||||
if [ -z "$ID" ]; then
|
if [ -z "$ID" ]; then
|
||||||
@@ -45,16 +30,3 @@ for f in "$DIR"/*.AppImage "$DIR"/*.deb "$DIR"/*.rpm "$DIR"/*-setup.exe "$DIR"/*
|
|||||||
done
|
done
|
||||||
echo "Release: ${API%/api/v1}/$REPO/releases/tag/$TAG"
|
echo "Release: ${API%/api/v1}/$REPO/releases/tag/$TAG"
|
||||||
|
|
||||||
# The update manifest, written only after the installer it points to is uploaded.
|
|
||||||
if [ -f "$SETUP.sig" ]; then
|
|
||||||
mkdir -p updater
|
|
||||||
node -e '
|
|
||||||
const [v, url, sigFile, notes] = process.argv.slice(1);
|
|
||||||
const signature = require("fs").readFileSync(sigFile, "utf8").trim();
|
|
||||||
const doc = { version: v, notes, pub_date: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
|
|
||||||
platforms: { "windows-x86_64": { signature, url } } };
|
|
||||||
require("fs").writeFileSync("updater/latest.json", JSON.stringify(doc, null, 2) + "\n");
|
|
||||||
' "$V" "${API%/api/v1}/$REPO/releases/download/$TAG/$(basename "$SETUP")" "$SETUP.sig" "${NOTES:-Kestrove $V}"
|
|
||||||
echo "Wrote updater/latest.json. To offer the update, commit it and push to main:"
|
|
||||||
echo " git add updater/latest.json && git commit -m \"Offer $V as an update\" && git push origin main"
|
|
||||||
fi
|
|
||||||
|
|||||||
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Puts the Windows installer on the Kestrove website and offers it as an update: signs it with the updater key if
|
||||||
|
# needed, copies it to the website's downloads/, and writes the website's updates/latest.json, which the app checks
|
||||||
|
# (src-tauri/src/updates.rs).
|
||||||
|
#
|
||||||
|
# TAURI_SIGNING_PRIVATE_KEY_PATH=… scripts/publish-update.sh [dir-with-installer] (default ../release/desktop)
|
||||||
|
#
|
||||||
|
# Sign after Authenticode signing: the updater signature covers the final file. The key's password is asked for,
|
||||||
|
# or read from TAURI_SIGNING_PRIVATE_KEY_PASSWORD. Optional NOTES: the text shown with the update. Optional
|
||||||
|
# WEBSITE_DIR: the website folder (default ../kestrove-website). Users get the update once the website is deployed.
|
||||||
|
set -eu
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
DIR=${1:-../release/desktop}
|
||||||
|
SITE=${WEBSITE_DIR:-../kestrove-website}
|
||||||
|
V=$(node -p 'require("./package.json").version')
|
||||||
|
NAME="Kestrove_${V}_x64-setup.exe"
|
||||||
|
SETUP="$DIR/$NAME"
|
||||||
|
[ -f "$SETUP" ] || { echo "$SETUP not found" >&2; exit 1; }
|
||||||
|
|
||||||
|
if [ ! -f "$SETUP.sig" ]; then
|
||||||
|
: "${TAURI_SIGNING_PRIVATE_KEY_PATH:?$SETUP.sig is missing: set TAURI_SIGNING_PRIVATE_KEY_PATH to the updater key to sign it}"
|
||||||
|
# --app-version binds the signature to this version (the app requires it), so an older signed installer can't
|
||||||
|
# be offered again under a newer version number.
|
||||||
|
npx tauri signer sign -f "$TAURI_SIGNING_PRIVATE_KEY_PATH" ${TAURI_SIGNING_PRIVATE_KEY_PASSWORD+-p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD"} \
|
||||||
|
--app-version "$V" "$SETUP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$SITE/downloads" "$SITE/updates"
|
||||||
|
cp "$SETUP" "$SITE/downloads/$NAME"
|
||||||
|
node -e '
|
||||||
|
const [out, v, url, sigFile, notes] = process.argv.slice(1);
|
||||||
|
const signature = require("fs").readFileSync(sigFile, "utf8").trim();
|
||||||
|
const doc = { version: v, notes, pub_date: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
|
||||||
|
platforms: { "windows-x86_64": { signature, url } } };
|
||||||
|
require("fs").writeFileSync(out, JSON.stringify(doc, null, 2) + "\n");
|
||||||
|
' "$SITE/updates/latest.json" "$V" "https://kestrove.samialabri.com/downloads/$NAME" "$SETUP.sig" "${NOTES:-Kestrove $V}"
|
||||||
|
echo "Copied $NAME to $SITE/downloads and wrote $SITE/updates/latest.json. Deploy the website to offer the update."
|
||||||
Executable
+15
@@ -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_<version>_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
|
||||||
@@ -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
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
@@ -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"
|
||||||
Generated
+1
-1
@@ -3064,7 +3064,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kestrove-desktop"
|
name = "kestrove-desktop"
|
||||||
version = "1.2.0"
|
version = "1.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kestrove-desktop"
|
name = "kestrove-desktop"
|
||||||
version = "1.2.0"
|
version = "1.2.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.82"
|
rust-version = "1.82"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
@@ -73,7 +73,7 @@ windows-sys = { version = "0.61", features = [
|
|||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_UI_WindowsAndMessaging",
|
"Win32_UI_WindowsAndMessaging",
|
||||||
] }
|
] }
|
||||||
# In-app updates (updates.rs): signed installers listed in latest.json on the Kestrove Gitea.
|
# In-app updates (updates.rs): signed installers listed in latest.json on the Kestrove website.
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
# Windows Hello (KeyCredentialManager); the same version Tauri already pulls in.
|
# Windows Hello (KeyCredentialManager); the same version Tauri already pulls in.
|
||||||
windows = { version = "0.62", features = ["Foundation", "Security_Credentials", "Security_Cryptography", "Storage_Streams"] }
|
windows = { version = "0.62", features = ["Foundation", "Security_Credentials", "Security_Cryptography", "Storage_Streams"] }
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "app.kestrove.nmh",
|
||||||
|
"description": "Kestrove native messaging host",
|
||||||
|
"path": "/usr/bin/kestrove-nmh",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": ["chrome-extension://pmhfoafjoogobhfadaicklhkldgnafmk/", "chrome-extension://kogapfibnnpeapfcjkkpgjddbhnjfein/"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "app.kestrove.nmh",
|
||||||
|
"description": "Kestrove native messaging host",
|
||||||
|
"path": "/usr/bin/kestrove-nmh",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_extensions": ["[email protected]"]
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ pub mod autolock;
|
|||||||
pub mod device_key;
|
pub mod device_key;
|
||||||
pub mod hello;
|
pub mod hello;
|
||||||
pub mod qr;
|
pub mod qr;
|
||||||
|
pub mod snap;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
pub mod title_bar;
|
pub mod title_bar;
|
||||||
pub mod updates;
|
pub mod updates;
|
||||||
|
|||||||
@@ -232,6 +232,14 @@ async fn create_account(
|
|||||||
sk.map(|s| s.to_string())
|
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.
|
/// `secret_key` may be omitted once this device remembers it.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn unlock(app: State<'_, App>, master_password: String, secret_key: Option<String>) -> CmdResult<()> {
|
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() {
|
fn main() {
|
||||||
|
kestrove_desktop::snap::register_native_host();
|
||||||
let builder = tauri::Builder::default();
|
let builder = tauri::Builder::default();
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||||
@@ -830,6 +839,7 @@ fn main() {
|
|||||||
state,
|
state,
|
||||||
set_title_bar_colors,
|
set_title_bar_colors,
|
||||||
create_account,
|
create_account,
|
||||||
|
create_local_account,
|
||||||
unlock,
|
unlock,
|
||||||
hello_unlock,
|
hello_unlock,
|
||||||
set_hello,
|
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)
|
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
|
/// For a vault created before sync was set up: registers it with the server (setup code, invite, or nothing
|
||||||
/// with open sign-up).
|
/// with open sign-up).
|
||||||
pub async fn register(&self, setup_code: &str, email: &str) -> Result<(), String> {
|
pub async fn register(&self, setup_code: &str, email: &str) -> Result<(), String> {
|
||||||
@@ -668,6 +679,19 @@ mod tests {
|
|||||||
assert!(!device_name().is_empty());
|
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]
|
#[tokio::test]
|
||||||
async fn settings_persist_and_secrets_stay_out_of_views() {
|
async fn settings_persist_and_secrets_stay_out_of_views() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! In-app updates on Windows. The Microsoft Store doesn't update EXE installs, so the app does it itself.
|
//! In-app updates on Windows. The Microsoft Store doesn't update EXE installs, so the app does it itself.
|
||||||
//!
|
//!
|
||||||
//! Tauri's updater fetches `latest.json` from the first address in tauri.conf.json → plugins.updater.endpoints
|
//! Tauri's updater fetches `latest.json` from the first address in tauri.conf.json → plugins.updater.endpoints
|
||||||
//! that answers (the Kestrove Gitea; a mirror can be added after it), and offers the newer version. Nothing is
|
//! that answers (the Kestrove website; a mirror can be added after it), and offers the newer version. Nothing is
|
||||||
//! downloaded until the user chooses "Update and restart". The installer must carry a signature from the
|
//! downloaded until the user chooses "Update and restart". The installer must carry a signature from the
|
||||||
//! Kestrove updater key, whose public half is in tauri.conf.json, so a compromised download server can hold
|
//! Kestrove updater key, whose public half is in tauri.conf.json, so a compromised download server can hold
|
||||||
//! updates back but can't ship one.
|
//! updates back but can't ship one.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Kestrove",
|
"productName": "Kestrove",
|
||||||
"version": "1.2.0",
|
"version": "1.2.2",
|
||||||
"identifier": "app.kestrove.desktop",
|
"identifier": "app.kestrove.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
@@ -41,6 +41,24 @@
|
|||||||
"icons/icon.ico",
|
"icons/icon.ico",
|
||||||
"icons/icon.png"
|
"icons/icon.png"
|
||||||
],
|
],
|
||||||
|
"linux": {
|
||||||
|
"deb": {
|
||||||
|
"files": {
|
||||||
|
"/usr/lib/mozilla/native-messaging-hosts/app.kestrove.nmh.json": "linux/app.kestrove.nmh.firefox.json",
|
||||||
|
"/etc/opt/chrome/native-messaging-hosts/app.kestrove.nmh.json": "linux/app.kestrove.nmh.chrome.json",
|
||||||
|
"/etc/chromium/native-messaging-hosts/app.kestrove.nmh.json": "linux/app.kestrove.nmh.chrome.json",
|
||||||
|
"/etc/opt/edge/native-messaging-hosts/app.kestrove.nmh.json": "linux/app.kestrove.nmh.chrome.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rpm": {
|
||||||
|
"files": {
|
||||||
|
"/usr/lib/mozilla/native-messaging-hosts/app.kestrove.nmh.json": "linux/app.kestrove.nmh.firefox.json",
|
||||||
|
"/etc/opt/chrome/native-messaging-hosts/app.kestrove.nmh.json": "linux/app.kestrove.nmh.chrome.json",
|
||||||
|
"/etc/chromium/native-messaging-hosts/app.kestrove.nmh.json": "linux/app.kestrove.nmh.chrome.json",
|
||||||
|
"/etc/opt/edge/native-messaging-hosts/app.kestrove.nmh.json": "linux/app.kestrove.nmh.chrome.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"windows": {
|
"windows": {
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"installerHooks": "windows/hooks.nsh",
|
"installerHooks": "windows/hooks.nsh",
|
||||||
@@ -53,7 +71,7 @@
|
|||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDZFRUM4RDQ2Mzk0Rjg5Q0IKUldUTGlVODVSbzNzYml1ZTN3d0pBN1IwWkRoZXNROTRmbk5WOE5CR2VJQVYrTUJLN21PNzI4eHEK",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDZFRUM4RDQ2Mzk0Rjg5Q0IKUldUTGlVODVSbzNzYml1ZTN3d0pBN1IwWkRoZXNROTRmbk5WOE5CR2VJQVYrTUJLN21PNzI4eHEK",
|
||||||
"requireSignedVersion": true,
|
"requireSignedVersion": true,
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://gitea.samialabri.com/kestrove/kestrove-desktop/raw/branch/main/updater/latest.json"
|
"https://kestrove.samialabri.com/updates/latest.json"
|
||||||
],
|
],
|
||||||
"windows": {
|
"windows": {
|
||||||
"installMode": "passive"
|
"installMode": "passive"
|
||||||
|
|||||||
+20
-5
@@ -208,7 +208,7 @@ type Tab = "phone" | "kit" | "paste" | "create";
|
|||||||
async function renderOnboarding(tab: Tab = "phone") {
|
async function renderOnboarding(tab: Tab = "phone") {
|
||||||
const status = await invoke<SyncStatus>("sync_status");
|
const status = await invoke<SyncStatus>("sync_status");
|
||||||
const body = stack();
|
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 nav = h("div", "seg", ...tabs.map(([t, label]) => {
|
||||||
const b = button(label, "quiet", async () => {
|
const b = button(label, "quiet", async () => {
|
||||||
await invoke("join_cancel");
|
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 pins = input({ placeholder: "base64, comma-separated", spellcheck: false }, true);
|
||||||
const pw = input({ type: "password", autocomplete: "new-password" });
|
const pw = input({ type: "password", autocomplete: "new-password" });
|
||||||
const pw2 = 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 err = errBox();
|
||||||
const btn = button("Create account", "primary");
|
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 () => {
|
btn.onclick = async () => {
|
||||||
err.replaceChildren();
|
err.replaceChildren();
|
||||||
if (pw.value !== pw2.value) {
|
if (pw.value !== pw2.value) {
|
||||||
@@ -430,25 +439,31 @@ function createPanel(body: HTMLElement, status: SyncStatus) {
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = "Creating…";
|
btn.textContent = "Creating…";
|
||||||
try {
|
try {
|
||||||
const secretKey = await invoke<string>("create_account", { serverUrl: url.value, pins: pins.value, setupCode: code.value, email: email.value, masterPassword: pw.value });
|
const secretKey = useServer.checked
|
||||||
|
? await invoke<string>("create_account", { serverUrl: url.value, pins: pins.value, setupCode: code.value, email: email.value, masterPassword: pw.value })
|
||||||
|
: await invoke<string>("create_local_account", { masterPassword: pw.value });
|
||||||
pw.value = pw2.value = "";
|
pw.value = pw2.value = "";
|
||||||
await showSecretKey(secretKey);
|
await showSecretKey(secretKey);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr(err, e);
|
setErr(err, e);
|
||||||
btn.disabled = false;
|
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).
|
// A vault created here whose server registration failed: continue to the vault (Settings → Sync retries).
|
||||||
if ((await invoke<AppState>("state")).has_account) {
|
if ((await invoke<AppState>("state")).has_account) {
|
||||||
body.append(button("Continue to the vault", "secondary", render));
|
body.append(button("Continue to the vault", "secondary", render));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
body.append(
|
serverFields.append(
|
||||||
p("kv-muted", "A new account on your Kestrove server. You'll get a Secret Key: keep it with your Emergency Kit."),
|
|
||||||
field("Your Kestrove server", url),
|
field("Your Kestrove server", url),
|
||||||
field("Email", email, "You sign in with it on new devices."),
|
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."),
|
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)),
|
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("Master password", pw, "10 or more characters."),
|
||||||
field("Repeat master password", pw2),
|
field("Repeat master password", pw2),
|
||||||
h("div", "actions", btn), err,
|
h("div", "actions", btn), err,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ h1, h2, h3 { margin: 0; }
|
|||||||
.grow { flex: 1 1 auto; min-width: 0; }
|
.grow { flex: 1 1 auto; min-width: 0; }
|
||||||
.row { display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }
|
.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 { 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 { display: flex; gap: var(--space-2); align-items: center; }
|
||||||
.actions.end { justify-content: flex-end; }
|
.actions.end { justify-content: flex-end; }
|
||||||
.center-row { display: flex; justify-content: center; }
|
.center-row { display: flex; justify-content: center; }
|
||||||
|
|||||||
Reference in New Issue
Block a user