feat!: persist ssh keys across builds by writing them in data dir
This commit is contained in:
parent
7a55e0defd
commit
3e44d24f9b
7 changed files with 145 additions and 57 deletions
|
@ -1,4 +1,11 @@
|
|||
{
|
||||
// Paths to SSH private keys to use for encryption
|
||||
"private_keys": {
|
||||
"ssh-rsa": "$DATA_DIR/ssh/id_rsa",
|
||||
"ecdsa-sha2-nistp256": "$DATA_DIR/ssh/id_ecdsa",
|
||||
"ssh-ed25519": "$DATA_DIR/ssh/id_ed25519"
|
||||
},
|
||||
|
||||
"keybindings": {
|
||||
"Home": {
|
||||
"<q>": "Quit", // Quit the application
|
||||
|
|
46
Cargo.lock
generated
46
Cargo.lock
generated
|
@ -1807,7 +1807,16 @@ version = "5.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1818,10 +1827,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"redox_users 0.4.6",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
|
@ -6075,6 +6096,17 @@ dependencies = [
|
|||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 2.0.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
|
@ -6899,6 +6931,15 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
|
@ -7117,6 +7158,7 @@ dependencies = [
|
|||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"signal-hook",
|
||||
"ssh-key",
|
||||
"strip-ansi-escapes",
|
||||
|
|
|
@ -27,6 +27,9 @@ blog = [
|
|||
"dep:patch-crate"
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 2
|
||||
|
||||
[package.metadata.patch]
|
||||
crates = ["ratatui-image"]
|
||||
|
||||
|
@ -76,6 +79,7 @@ russh = "0.49.2"
|
|||
rust-embed = { version = "8.7.2", features = ["actix"] }
|
||||
serde = { version = "1.0.211", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
ssh-key = { version = "0.6.7", features = ["getrandom", "crypto"] }
|
||||
signal-hook = "0.3.17"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
|
@ -85,10 +89,10 @@ tracing = "0.1.40"
|
|||
tracing-error = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] }
|
||||
tui-markdown = { version = "0.3.5", optional = true }
|
||||
shellexpand = "3.1.1"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.90"
|
||||
atrium-codegen = { git = "https://github.com/atrium-rs/atrium.git", rev = "ccc0213", optional = true }
|
||||
patch-crate = { version = "0.1.13", optional = true }
|
||||
ssh-key = { version = "0.6.7", features = ["getrandom", "crypto"] }
|
||||
vergen-gix = { version = "1.0.2", features = ["build", "cargo"] }
|
||||
|
|
25
build.rs
25
build.rs
|
@ -1,19 +1,12 @@
|
|||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use ssh_key::{rand_core, Algorithm, EcdsaCurve, LineEnding, PrivateKey};
|
||||
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder};
|
||||
|
||||
#[cfg(feature = "blog")]
|
||||
const ATPROTO_LEXICON_DIR: &str = "src/atproto/lexicons";
|
||||
#[cfg(feature = "blog")]
|
||||
const ATPROTO_CLIENT_DIR: &str = "src/atproto";
|
||||
const SSH_KEY_ALGOS: &[(&str, Algorithm)] = &[
|
||||
("rsa.pem", Algorithm::Rsa { hash: None }),
|
||||
("ed25519.pem", Algorithm::Ed25519),
|
||||
("ecdsa.pem", Algorithm::Ecdsa { curve: EcdsaCurve::NistP256 }),
|
||||
];
|
||||
|
||||
fn main() -> Result<()> {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
@ -29,24 +22,6 @@ fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
// Generate openSSH host keys
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let mut rng = rand_core::OsRng;
|
||||
for (file_name, algo) in SSH_KEY_ALGOS {
|
||||
let path = out_dir.join(file_name);
|
||||
if path.exists() {
|
||||
println!(
|
||||
"cargo:warning=Skipping existing host key: {:?}",
|
||||
path.file_stem().unwrap()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let key =
|
||||
PrivateKey::random(&mut rng, algo.to_owned()).map_err(anyhow::Error::from)?;
|
||||
key.write_openssh_file(&path, LineEnding::default())?;
|
||||
}
|
||||
|
||||
// Generate ATProto client with lexicon validation
|
||||
#[cfg(feature = "blog")]
|
||||
atrium_codegen::genapi(
|
||||
|
|
|
@ -15,7 +15,7 @@ use tokio_util::sync::CancellationToken;
|
|||
use tracing::debug;
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::components::*;
|
||||
use crate::{components::*, CONFIG};
|
||||
use crate::config::Config;
|
||||
use crate::keycode::KeyCodeExt;
|
||||
use crate::tui::terminal::{TerminalInfo, TerminalKind, UnsupportedReason};
|
||||
|
@ -90,7 +90,7 @@ impl App {
|
|||
should_suspend: false,
|
||||
needs_resize: false,
|
||||
|
||||
config: Config::new()?,
|
||||
config: CONFIG.clone(),
|
||||
mode: Mode::Home,
|
||||
last_tick_key_events: Vec::new(),
|
||||
action_tx,
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
#![allow(dead_code)] // Remove this once you start using the code
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::{env, io};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use derive_deref::{Deref, DerefMut};
|
||||
use directories::ProjectDirs;
|
||||
use directories::{ProjectDirs, UserDirs};
|
||||
use lazy_static::lazy_static;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use serde::de::Deserializer;
|
||||
use serde::Deserialize;
|
||||
use tracing::error;
|
||||
use serde::{de::Deserializer, Deserialize};
|
||||
use ssh_key::{rand_core, Algorithm, LineEnding, PrivateKey};
|
||||
use tracing::{debug, error, info, info_span};
|
||||
|
||||
use crate::action::Action;
|
||||
use crate::app::Mode;
|
||||
|
@ -31,6 +32,8 @@ pub struct AppConfig {
|
|||
pub struct Config {
|
||||
#[serde(default, flatten)]
|
||||
pub config: AppConfig,
|
||||
#[serde(default, deserialize_with = "private_key_deserialize")]
|
||||
pub private_keys: Vec<PrivateKey>,
|
||||
#[serde(default)]
|
||||
pub keybindings: KeyBindings,
|
||||
#[serde(default)]
|
||||
|
@ -39,9 +42,9 @@ pub struct Config {
|
|||
|
||||
lazy_static! {
|
||||
pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string();
|
||||
pub static ref DATA_FOLDER: Option<PathBuf> =
|
||||
pub static ref DATA_DIR: Option<PathBuf> =
|
||||
env::var(format!("{}_DATA", PROJECT_NAME.clone())).ok().map(PathBuf::from);
|
||||
pub static ref CONFIG_FOLDER: Option<PathBuf> =
|
||||
pub static ref CONFIG_DIR: Option<PathBuf> =
|
||||
env::var(format!("{}_CONFIG", PROJECT_NAME.clone())).ok().map(PathBuf::from);
|
||||
}
|
||||
|
||||
|
@ -50,6 +53,10 @@ impl Config {
|
|||
let default_config: Config = json5::from_str(CONFIG).unwrap();
|
||||
let data_dir = get_data_dir();
|
||||
let config_dir = get_config_dir();
|
||||
|
||||
info!("Using data directory: {}", data_dir.display());
|
||||
info!("Using config directory: {}", config_dir.display());
|
||||
|
||||
let mut builder = config::Config::builder()
|
||||
.set_default("data_dir", data_dir.to_str().unwrap())?
|
||||
.set_default("config_dir", config_dir.to_str().unwrap())?;
|
||||
|
@ -88,13 +95,18 @@ impl Config {
|
|||
user_styles.entry(style_key.clone()).or_insert(*style);
|
||||
}
|
||||
}
|
||||
if cfg.private_keys.is_empty() {
|
||||
for key in default_config.private_keys {
|
||||
cfg.private_keys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_data_dir() -> PathBuf {
|
||||
let directory = if let Some(s) = DATA_FOLDER.clone() {
|
||||
let directory = if let Some(s) = DATA_DIR.clone() {
|
||||
s
|
||||
} else if let Some(proj_dirs) = project_directory() {
|
||||
proj_dirs.data_local_dir().to_path_buf()
|
||||
|
@ -105,7 +117,7 @@ pub fn get_data_dir() -> PathBuf {
|
|||
}
|
||||
|
||||
pub fn get_config_dir() -> PathBuf {
|
||||
let directory = if let Some(s) = CONFIG_FOLDER.clone() {
|
||||
let directory = if let Some(s) = CONFIG_DIR.clone() {
|
||||
s
|
||||
} else if let Some(proj_dirs) = project_directory() {
|
||||
proj_dirs.config_local_dir().to_path_buf()
|
||||
|
@ -116,7 +128,60 @@ pub fn get_config_dir() -> PathBuf {
|
|||
}
|
||||
|
||||
fn project_directory() -> Option<ProjectDirs> {
|
||||
ProjectDirs::from("com", "kdheepak", env!("CARGO_PKG_NAME"))
|
||||
ProjectDirs::from("xyz", "devcomp", env!("CARGO_PKG_NAME"))
|
||||
}
|
||||
|
||||
fn private_key_deserialize<'de, D>(deserializer: D) -> Result<Vec<PrivateKey>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>, {
|
||||
let keys = HashMap::<String, String>::deserialize(deserializer)?
|
||||
.into_iter()
|
||||
.map(|(pem_type, pem_or_path)| {
|
||||
debug!("Loading {} private key", pem_type);
|
||||
PrivateKey::from_openssh(pem_or_path.as_bytes()).or_else(|_| {
|
||||
debug!("Failed to parse {} PEM from string, trying as file path", pem_type);
|
||||
|
||||
let ud = UserDirs::new().ok_or(ssh_key::Error::Io(io::ErrorKind::NotFound))?;
|
||||
let expanded_path = PathBuf::from(&*shellexpand::full_with_context_no_errors(
|
||||
&pem_or_path,
|
||||
|| ud.home_dir().to_str(),
|
||||
|var| match var {
|
||||
"DATA_DIR" => get_data_dir().to_str().map(|s| s.to_string()),
|
||||
"CONFIG_DIR" => get_config_dir().to_str().map(|s| s.to_string()),
|
||||
"HOME" => Some(String::from("~")),
|
||||
_ => None,
|
||||
},
|
||||
));
|
||||
|
||||
if !expanded_path.exists() {
|
||||
let span = info_span!("host_keygen", algo = %pem_type, path = %expanded_path.display());
|
||||
let _lock = span.enter();
|
||||
|
||||
if let Some(parent) = expanded_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
info!("Generating key... (this may take a while)");
|
||||
let algo = Algorithm::from_str(&pem_type)?;
|
||||
let key = PrivateKey::random(&mut rand_core::OsRng, algo.to_owned())?;
|
||||
key.write_openssh_file(&expanded_path, LineEnding::default())?;
|
||||
|
||||
return Ok(key);
|
||||
}
|
||||
|
||||
PrivateKey::read_openssh_file(&expanded_path)
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<PrivateKey>, ssh_key::Error>>()
|
||||
.map_err(serde::de::Error::custom)
|
||||
.inspect_err(|err| error!("Loading private keys error: {}", err))?;
|
||||
|
||||
if keys.is_empty() {
|
||||
return Err(serde::de::Error::custom("No valid private keys found in configuration"))
|
||||
.inspect_err(|err| error!("{}", err));
|
||||
}
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
|
|
29
src/main.rs
29
src/main.rs
|
@ -5,8 +5,7 @@ use cli::Cli;
|
|||
use color_eyre::eyre::eyre;
|
||||
use color_eyre::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use russh::keys::PrivateKey;
|
||||
use russh::server::Config;
|
||||
use russh::server::Config as SshConfig;
|
||||
use russh::MethodSet;
|
||||
use ssh::SshServer;
|
||||
|
||||
|
@ -14,7 +13,9 @@ use ssh::SshServer;
|
|||
pub(crate) use atproto::com;
|
||||
#[cfg(feature = "blog")]
|
||||
pub(crate) use atrium_api::*;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::landing::WebLandingServer;
|
||||
|
||||
mod action;
|
||||
|
@ -31,14 +32,9 @@ mod logging;
|
|||
mod ssh;
|
||||
mod tui;
|
||||
|
||||
const SSH_KEYS: &[&[u8]] = &[
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/rsa.pem")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/ecdsa.pem")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/ed25519.pem")),
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
lazy_static! {
|
||||
pub(crate) static ref CONFIG: Config = Config::new().expect("Config loading error, see above");
|
||||
pub(crate) static ref OPTIONS: Cli = Cli::parse();
|
||||
pub(crate) static ref SSH_SOCKET_ADDR: Option<SocketAddr> = Some(SocketAddr::from((host_ip().ok()?, OPTIONS.ssh_port)));
|
||||
pub(crate) static ref WEB_SERVER_ADDR: Option<SocketAddr> = Some(SocketAddr::from((host_ip().ok()?, OPTIONS.web_port)));
|
||||
|
@ -53,8 +49,9 @@ async fn main() -> Result<()> {
|
|||
let ssh_socket_addr = SSH_SOCKET_ADDR.ok_or(eyre!("Invalid host IP provided"))?;
|
||||
let web_server_addr = WEB_SERVER_ADDR.ok_or(eyre!("Invalid host IP provided"))?;
|
||||
|
||||
let ssh_config = tokio::task::block_in_place(ssh_config);
|
||||
tokio::select! {
|
||||
ssh_res = SshServer::start(ssh_socket_addr, ssh_config()) => ssh_res,
|
||||
ssh_res = SshServer::start(ssh_socket_addr, ssh_config) => ssh_res,
|
||||
web_res = WebLandingServer::start(web_server_addr) => web_res.map_err(|err| eyre!(err)),
|
||||
}
|
||||
}
|
||||
|
@ -75,17 +72,15 @@ pub fn host_ip() -> Result<[u8; 4]> {
|
|||
.map_err(|_| eyre!("Invalid host IP provided"))
|
||||
}
|
||||
|
||||
fn ssh_config() -> Config {
|
||||
let conf = Config {
|
||||
#[instrument]
|
||||
fn ssh_config() -> SshConfig {
|
||||
let conf = SshConfig {
|
||||
methods: MethodSet::NONE,
|
||||
keys: SSH_KEYS
|
||||
.to_vec()
|
||||
.iter()
|
||||
.filter_map(|pem| PrivateKey::from_openssh(pem).ok())
|
||||
.collect(),
|
||||
keys: CONFIG.private_keys.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
tracing::trace!("SSH config: {:#?}", conf);
|
||||
tracing::info!("SSH will use {} host keys", conf.keys.len());
|
||||
tracing::trace!("SSH config: {:?}", conf);
|
||||
conf
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue