Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
6 changes: 3 additions & 3 deletions update/Cargo.toml → update/binary-updater/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
name = "secluso-update"
version = "1.0.2"
edition = "2021"
authors = ["Ardalan Amiri Sani <arrdalan@gmail.com>"]
edition = "2024"
authors = ["Ardalan Amiri Sani <arrdalan@proton.me>", "John Kaczman <jkaczman@pm.me>"]

[dependencies]
anyhow = "1"
Expand All @@ -21,4 +21,4 @@ sequoia-openpgp = { version = "2.2.0", default-features = false, features = ["co
# zip + hashing
zip = "8"
sha2 = "0.10"
hex = "0.4"
hex = "0.4"
8 changes: 4 additions & 4 deletions update/src/lib.rs → update/binary-updater/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! SPDX-License-Identifier: GPL-3.0-or-later

use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use bytes::Bytes;
use reqwest::blocking::Client;
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
use semver::Version;
use serde::Deserialize;
use sha2::{Digest, Sha256};
Expand All @@ -15,10 +15,10 @@ use std::time::{Duration, Instant};
use zip::ZipArchive;

use openpgp::cert::Cert;
use openpgp::parse::Parse;
use openpgp::parse::stream::{
DetachedVerifierBuilder, GoodChecksum, MessageLayer, MessageStructure, VerificationHelper,
};
use openpgp::parse::Parse;
use openpgp::policy::StandardPolicy;
use openpgp::{Fingerprint, KeyHandle};
use sequoia_openpgp as openpgp;
Expand All @@ -40,7 +40,7 @@ pub const NUM_USERNAME_CHARS: usize = 14;
pub const NUM_PASSWORD_CHARS: usize = 14;

/// A signer entry: label controls signature filename, github_user controls accepted keyring source.
///Fingerprint (optionally in developer mode) pins trust to one exact OpenPGP primary key
/// Fingerprint (optionally in developer mode) pins trust to one exact OpenPGP primary key
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Signer {
pub label: String,
Expand Down
89 changes: 43 additions & 46 deletions update/src/main.rs → update/binary-updater/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ use std::thread::sleep;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use secluso_update::{
build_github_client, default_signers, download_and_verify_component, fetch_latest_release,
get_current_version, github_token_from_env, parse_sig_keys, require_release_is_immutable,
write_current_version, Component, DEFAULT_OWNER_REPO,
Component, DEFAULT_OWNER_REPO, build_github_client, default_signers,
download_and_verify_component, fetch_latest_release, get_current_version,
github_token_from_env, parse_sig_keys, require_release_is_immutable, write_current_version,
};

const USAGE: &str = r#"
const USAGE: &str = r"
Secluso updater.

Usage:
Expand All @@ -42,7 +42,7 @@ Options:
--hint-check-interval-secs N Update hint poll interval seconds [default: 10].
--version, -v Show tool version.
--help, -h Show this screen.
"#;
";

#[derive(Debug, Deserialize)]
struct Args {
Expand Down Expand Up @@ -125,14 +125,14 @@ fn main() -> ! {
if args.flag_once {
println!("Going to check for updates.");
if let Err(e) = check_update(&args) {
eprintln!("Update check failed: {:#}", e);
eprintln!("Update check failed: {e:#}");
std::process::exit(1);
}
std::process::exit(0);
}

if let Some(ref update_hint_path) = args.flag_update_hint_path {
if args.flag_interval_secs % args.flag_hint_check_interval_secs != 0 {
if !args.flag_interval_secs.is_multiple_of(args.flag_hint_check_interval_secs) {
eprintln!(
"flag_interval_secs ({}) must be divisible by flag_update_hint_interval_secs ({})",
args.flag_interval_secs, args.flag_hint_check_interval_secs
Expand All @@ -141,25 +141,25 @@ fn main() -> ! {
}

// We want to force a check in the beginning.
let mut last_full_check = Instant::now() - Duration::from_secs(args.flag_interval_secs);
let mut last_full_check = Instant::now().checked_sub(Duration::from_secs(args.flag_interval_secs)).unwrap();

loop {
let elapsed = last_full_check.elapsed();
if elapsed >= Duration::from_secs(args.flag_interval_secs) {
println!("Scheduled update check.");
if let Err(e) = check_update(&args) {
eprintln!("Update check failed: {:#}", e);
eprintln!("Update check failed: {e:#}");
}
last_full_check = Instant::now();
continue;
}

sleep(Duration::from_secs(args.flag_hint_check_interval_secs));

if is_there_update_hint(&update_hint_path) {
if is_there_update_hint(update_hint_path) {
println!("Update hint received, triggering early check.");
if let Err(e) = check_update(&args) {
eprintln!("Update check failed: {:#}", e);
eprintln!("Update check failed: {e:#}");
}
last_full_check = Instant::now();
}
Expand All @@ -168,13 +168,14 @@ fn main() -> ! {
loop {
println!("Going to check for updates.");
if let Err(e) = check_update(&args) {
eprintln!("Update check failed: {:#}", e);
eprintln!("Update check failed: {e:#}");
}
sleep(Duration::from_secs(args.flag_interval_secs));
}
}
}

#[must_use]
pub fn is_there_update_hint(path: &str) -> bool {
let p = Path::new(path);

Expand Down Expand Up @@ -229,7 +230,7 @@ fn check_update(args: &Args) -> Result<()> {
ReleaseSource::LatestImmutableGitHub => {
println!("Using the latest immutable GitHub release.");
}
};
}

let release = selected_release.release;

Expand Down Expand Up @@ -264,7 +265,7 @@ fn check_update(args: &Args) -> Result<()> {
prepare_verified_component_install(Path::new(&final_path), &verified.component_bytes)?;

if let Some(unit) = args.flag_restart_unit.as_deref() {
println!("Stopping unit: {}", unit);
println!("Stopping unit: {unit}");
run(&format!("systemctl stop {}", shell_escape(unit)));
}

Expand All @@ -276,7 +277,7 @@ fn check_update(args: &Args) -> Result<()> {
prepared_install.commit()?;

if let Some(unit) = args.flag_restart_unit.as_deref() {
println!("Starting unit: {}", unit);
println!("Starting unit: {unit}");
run(&format!("systemctl start {}", shell_escape(unit)));
}

Expand Down Expand Up @@ -306,8 +307,7 @@ fn prepare_verified_component_install(

let target_name = final_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "secluso-binary".to_string());
.map_or_else(|| "secluso-binary".to_string(), |name| name.to_string_lossy().into_owned());

// Create the temp inode ourselves in the protected target directory with exclusive creation.
// *Even if the filename is predictable, create_new refuses to adopt a preexisting file*
Expand Down Expand Up @@ -370,7 +370,7 @@ fn create_secure_install_temp_file(
.open(&tmp_path)
{
Ok(file) => return Ok((tmp_path, file)),
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {},
Err(err) => {
return Err(err)
.with_context(|| format!("creating temp install path {}", tmp_path.display()));
Expand All @@ -385,7 +385,7 @@ fn create_secure_install_temp_file(
}

fn select_release_for_component<FLatest, FRequireImmutable>(
component: Component,
_component: Component,
current_version: &Version,
fetch_latest_release_fn: FLatest,
require_release_is_immutable_fn: FRequireImmutable,
Expand All @@ -394,23 +394,19 @@ where
FLatest: FnOnce() -> Result<secluso_update::GhRelease>,
FRequireImmutable: FnOnce(&secluso_update::GhRelease) -> Result<()>,
{
match component {
_ => {
let release = fetch_latest_release_fn()?;
require_release_is_immutable_fn(&release)?;

let latest_version = release.parsed_version()?;
if current_version >= &latest_version {
println!("Already on the latest immutable GitHub release.");
return Ok(None);
}
let release = fetch_latest_release_fn()?;
require_release_is_immutable_fn(&release)?;

Ok(Some(SelectedRelease {
release,
source: ReleaseSource::LatestImmutableGitHub,
}))
}
let latest_version = release.parsed_version()?;
if current_version >= &latest_version {
println!("Already on the latest immutable GitHub release.");
return Ok(None);
}

Ok(Some(SelectedRelease {
release,
source: ReleaseSource::LatestImmutableGitHub,
}))
}

// "best-effort" command runner used for service stop/start so updater can still finish installation
Expand All @@ -423,6 +419,18 @@ fn run(cmd: &str) {
.status();
}


// Minimal shell escaping helper to safely embed unit names into sh -c commands.
fn shell_escape(s: &str) -> String {
if s.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_' | '@'))
{
s.to_string()
} else {
format!("'{}'", s.replace('\'', r"'\''"))
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -501,15 +509,4 @@ mod tests {
assert!(!tmp_path.exists());
assert!(!final_path.exists());
}
}

// Minimal shell escaping helper to safely embed unit names into sh -c commands.
fn shell_escape(s: &str) -> String {
if s.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_' | '@'))
{
s.to_string()
} else {
format!("'{}'", s.replace('\'', r#"'\''"#))
}
}
}
Loading
Loading