Skip to main content

kovra_core/
exchange.rs

1//! USB offline-exchange kit (KOV-41/42/43, §7.3) — the on-USB file/script
2//! contract plus the *pure* builders that generate it. The OS-touching pieces
3//! (formatting the stick, discovering its mount point) live at the CLI edge
4//! behind the [`Formatter`](crate::Formatter) trait; everything here is
5//! deterministic and unit-tested.
6//!
7//! ## On-USB layout (the contract)
8//!
9//! ```text
10//! <USB>/
11//!   kovra            the macOS binary (origin drops it; destination installs it)
12//!   install.sh       destination bootstrap (install + passphrase vault + keygen)
13//!   recipient.pub    destination writes its OpenSSH public key here (handed back)
14//!   package.kovra    origin seals the package here (exchange seal, KOV-42)
15//!   unpack.sh        origin writes the destination open helper here (KOV-42/43)
16//! ```
17//!
18//! The access **token** is never written to the USB — it travels out-of-band
19//! (a second channel), per §7.2.
20
21use std::path::{Path, PathBuf};
22
23use crate::error::CoreError;
24
25/// The bundled binary's name on the USB.
26pub const BINARY_NAME: &str = "kovra";
27/// Destination bootstrap script.
28pub const INSTALL_SCRIPT: &str = "install.sh";
29/// Destination open helper (written by `exchange seal`).
30pub const UNPACK_SCRIPT: &str = "unpack.sh";
31/// Where the destination writes its OpenSSH public key for the origin to seal to.
32pub const RECIPIENT_PUB: &str = "recipient.pub";
33/// The sealed package the origin writes for the destination.
34pub const PACKAGE_FILE: &str = "package.kovra";
35
36/// The custodied recipient identity coordinate. Fixed so all three steps agree:
37/// the destination `keygen`s it (install.sh), the origin seals to its public
38/// half (`exchange seal`), and the destination opens with it
39/// (`unpack --identity`, KOV-39).
40pub const RECIPIENT_COORDINATE: &str = "secret:exchange/recipient/key";
41
42/// The ExFAT volume label kovra gives the bootstrap USB. Fixed (uppercase,
43/// FAT-safe) so the mount point is predictable on the destination.
44pub const VOLUME_LABEL: &str = "KOVRA";
45
46/// The macOS mount point of a freshly-formatted exchange USB (`/Volumes/KOVRA`).
47/// `[host]` path convention; used by `exchange init`/`open` to populate/read the
48/// stick after a format.
49#[must_use]
50pub fn mount_point() -> PathBuf {
51    Path::new("/Volumes").join(VOLUME_LABEL)
52}
53
54/// The destination bootstrap script (`install.sh`). Run from the USB on the
55/// destination Mac, it: installs the bundled `kovra`, clears the macOS
56/// quarantine flag on the unsigned binary, creates a **portable passphrase
57/// vault** (no OS keychain / Touch ID dependency), generates the recipient
58/// keypair, and writes `recipient.pub` back to the USB for the origin to seal
59/// against. Pure — no secret material is embedded (the passphrase is prompted on
60/// the destination, never travels).
61#[must_use]
62pub fn render_install_script() -> String {
63    format!(
64        r##"#!/usr/bin/env bash
65# kovra offline-exchange — destination bootstrap (origin-generated).
66#
67# Installs kovra from this USB, creates a PORTABLE passphrase vault (no Touch ID
68# needed), generates your recipient keypair, and writes {pub} back to this USB so
69# the sender can seal a package to you. The access token arrives separately (a
70# second channel) — never on this USB.
71#
72#   Run it from the USB:   ./{install}
73set -euo pipefail
74
75HERE="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
76BIN_DIR="${{KOVRA_BIN_DIR:-$HOME/.local/bin}}"
77mkdir -p "$BIN_DIR"
78cp "$HERE/{binary}" "$BIN_DIR/{binary}"
79chmod +x "$BIN_DIR/{binary}"
80# Clear the macOS quarantine flag on the bundled (unsigned) binary.
81xattr -d com.apple.quarantine "$BIN_DIR/{binary}" 2>/dev/null || true
82export PATH="$BIN_DIR:$PATH"
83
84# A portable vault keyed by a passphrase — no OS keychain, works on any Mac.
85if [ -z "${{KOVRA_PASSPHRASE:-}}" ]; then
86  printf 'Choose a vault passphrase (you will need it to open the package): '
87  read -r -s KOVRA_PASSPHRASE; printf '\n'
88  export KOVRA_PASSPHRASE
89fi
90
91kovra init
92kovra keygen '{coord}' --type ed25519 --sensitivity high \
93  --description 'kovra offline-exchange recipient identity'
94kovra pubkey '{coord}' > "$HERE/{pub}"
95
96echo
97echo "{pub} written to the USB. Hand the USB back to the sender so they can run"
98echo "'kovra exchange seal'. Keep your passphrase — you'll need it to open the package."
99"##,
100        binary = BINARY_NAME,
101        install = INSTALL_SCRIPT,
102        pub = RECIPIENT_PUB,
103        coord = RECIPIENT_COORDINATE,
104    )
105}
106
107/// The destination **open** helper (`unpack.sh`), written to the USB by
108/// `exchange seal`. Run on the destination, it opens `package.kovra` with the
109/// custodied recipient identity (KOV-39) and imports the secrets, prompting for
110/// the vault passphrase. For `high` entries the access token (the second
111/// channel) is supplied via `KOVRA_EXCHANGE_TOKEN` and written to a temp file
112/// **outside** the USB (deleted on exit) — the token never lands on the stick.
113/// Pure; embeds no secret.
114#[must_use]
115pub fn render_unpack_script() -> String {
116    format!(
117        r##"#!/usr/bin/env bash
118# kovra offline-exchange — destination OPEN helper (origin-generated).
119#
120# Opens {package} on this USB with your custodied recipient identity and imports
121# the secrets. You'll be asked for your vault passphrase. For `high` entries,
122# supply the access token the sender sent over a SEPARATE channel:
123#   export KOVRA_EXCHANGE_TOKEN=...   (or use `kovra exchange open`)
124set -euo pipefail
125
126HERE="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
127if [ -z "${{KOVRA_PASSPHRASE:-}}" ]; then
128  printf 'Vault passphrase: '
129  read -r -s KOVRA_PASSPHRASE; printf '\n'
130  export KOVRA_PASSPHRASE
131fi
132
133args=(unpack --in "$HERE/{package}" --identity '{coord}')
134if [ -n "${{KOVRA_EXCHANGE_TOKEN:-}}" ]; then
135  # Land the token in a temp file OFF the USB; never written to the stick.
136  tok="$(mktemp -t kovra-token)"
137  trap 'rm -f "$tok"' EXIT
138  printf '%s' "$KOVRA_EXCHANGE_TOKEN" > "$tok"
139  args+=(--token "$tok")
140fi
141
142kovra "${{args[@]}}"
143echo "Imported. The secrets now live in your local vault."
144"##,
145        package = PACKAGE_FILE,
146        coord = RECIPIENT_COORDINATE,
147    )
148}
149
150/// Populate a freshly-formatted USB (mounted at `dest`) with the bootstrap kit:
151/// copy the `kovra` binary and write an executable `install.sh`. OS-agnostic
152/// (plain file I/O), so it is unit-tested against a temp dir — the *format* that
153/// precedes it is the `[host]` step.
154pub fn write_bootstrap(
155    dest: &Path,
156    kovra_binary: &Path,
157    install_script: &str,
158) -> Result<(), CoreError> {
159    std::fs::create_dir_all(dest)
160        .map_err(|e| CoreError::Io(format!("creating {}: {e}", dest.display())))?;
161
162    let bin_dst = dest.join(BINARY_NAME);
163    std::fs::copy(kovra_binary, &bin_dst).map_err(|e| {
164        CoreError::Io(format!(
165            "copying {} to {}: {e}",
166            kovra_binary.display(),
167            bin_dst.display()
168        ))
169    })?;
170    make_executable(&bin_dst)?;
171
172    let script_dst = dest.join(INSTALL_SCRIPT);
173    std::fs::write(&script_dst, install_script)
174        .map_err(|e| CoreError::Io(format!("writing {}: {e}", script_dst.display())))?;
175    make_executable(&script_dst)?;
176
177    Ok(())
178}
179
180/// `chmod +x` (0755) on Unix; a no-op elsewhere.
181fn make_executable(path: &Path) -> Result<(), CoreError> {
182    #[cfg(unix)]
183    {
184        use std::os::unix::fs::PermissionsExt;
185        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))
186            .map_err(|e| CoreError::Io(format!("chmod +x {}: {e}", path.display())))?;
187    }
188    #[cfg(not(unix))]
189    {
190        let _ = path;
191    }
192    Ok(())
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn install_script_drives_the_destination_bootstrap() {
201        let s = render_install_script();
202        assert!(s.starts_with("#!/usr/bin/env bash"), "has a shebang");
203        assert!(s.contains("set -euo pipefail"), "fails fast");
204        // Installs the bundled binary and clears quarantine.
205        assert!(s.contains(&format!("cp \"$HERE/{BINARY_NAME}\"")));
206        assert!(s.contains("com.apple.quarantine"));
207        // Portable passphrase vault (no Touch ID), prompted not embedded.
208        assert!(s.contains("KOVRA_PASSPHRASE"));
209        assert!(s.contains("kovra init"));
210        // Generates the agreed recipient identity and writes the pubkey to the USB.
211        assert!(s.contains(RECIPIENT_COORDINATE));
212        assert!(s.contains(&format!("\"$HERE/{RECIPIENT_PUB}\"")));
213        // No secret material is embedded in the generated script.
214        assert!(!s.to_lowercase().contains("private key"));
215    }
216
217    #[test]
218    fn write_bootstrap_copies_binary_and_writes_executable_script() {
219        let tmp = tempfile::tempdir().unwrap();
220        let dest = tmp.path().join("KOVRA");
221        let fake_bin = tmp.path().join("kovra-bin");
222        std::fs::write(&fake_bin, b"#!/bin/sh\necho kovra\n").unwrap();
223
224        write_bootstrap(&dest, &fake_bin, &render_install_script()).unwrap();
225
226        let bin = dest.join(BINARY_NAME);
227        let script = dest.join(INSTALL_SCRIPT);
228        assert!(bin.exists() && script.exists());
229        assert_eq!(std::fs::read(&bin).unwrap(), b"#!/bin/sh\necho kovra\n");
230        assert!(
231            std::fs::read_to_string(&script)
232                .unwrap()
233                .contains("kovra keygen")
234        );
235
236        #[cfg(unix)]
237        {
238            use std::os::unix::fs::PermissionsExt;
239            for p in [&bin, &script] {
240                let mode = std::fs::metadata(p).unwrap().permissions().mode();
241                assert!(mode & 0o111 != 0, "{} must be executable", p.display());
242            }
243        }
244    }
245
246    #[test]
247    fn mount_point_is_volumes_kovra() {
248        assert_eq!(mount_point(), Path::new("/Volumes/KOVRA"));
249    }
250
251    #[test]
252    fn unpack_script_opens_with_recipient_identity_and_offusb_token() {
253        let s = render_unpack_script();
254        assert!(s.starts_with("#!/usr/bin/env bash"));
255        assert!(s.contains(&format!("--in \"$HERE/{PACKAGE_FILE}\"")));
256        assert!(s.contains(&format!("--identity '{RECIPIENT_COORDINATE}'")));
257        // The token (second channel) is taken from the env and landed OFF the USB.
258        assert!(s.contains("KOVRA_EXCHANGE_TOKEN"));
259        assert!(s.contains("mktemp"));
260        // The token file is cleaned up and never written to the stick.
261        assert!(s.contains("rm -f \"$tok\""));
262        assert!(!s.to_lowercase().contains("private key"));
263    }
264}