fix(cli): hide store passphrase like a password prompt
All checks were successful
ci / test (pull_request) Successful in 4m54s
All checks were successful
ci / test (pull_request) Successful in 4m54s
stdin read_line echoed the passphrase. rpassword disables tty echo. ONIONWIRE_STORE_PASSPHRASE still skips the prompt.
This commit is contained in:
parent
f109223678
commit
c5019f1737
7 changed files with 113 additions and 25 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
|
@ -2639,6 +2639,7 @@ dependencies = [
|
|||
"md-5",
|
||||
"rand 0.8.8",
|
||||
"ratatui",
|
||||
"rpassword",
|
||||
"rusqlite",
|
||||
"safelog",
|
||||
"serde_json",
|
||||
|
|
@ -3381,6 +3382,17 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "7.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rtoolbox",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.10"
|
||||
|
|
@ -3402,6 +3414,16 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtoolbox"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.36.0"
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ argon2 = "0.5"
|
|||
chacha20poly1305 = "0.10"
|
||||
sha3 = "0.10"
|
||||
md-5 = "0.10"
|
||||
rpassword = "7"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ onionwire
|
|||
|
||||
or `cargo run --release`.
|
||||
|
||||
On start you are prompted for a store passphrase (or set `ONIONWIRE_STORE_PASSPHRASE`). Empty passphrase is rejected; a wrong passphrase does not open chat. Then you should see `onionwire: bootstrapping Arti…` on stderr. Directory bootstrap is usually under a minute; the onion is ready once a probe connect works (combined Arti status may still say Bootstrapping). Fail closed at 360s. Data lives in `ONIONWIRE_HOME` if set, otherwise `~/.local/share/onionwire/` (`onionwire.db` + Arti state, mode 0700). First open creates an ed25519 identity key. That key **is** you.
|
||||
On start you are prompted for a store passphrase (echo off, like other CLI passwords) or set `ONIONWIRE_STORE_PASSPHRASE`. Empty passphrase is rejected; a wrong passphrase does not open chat. Then you should see `onionwire: bootstrapping Arti…` on stderr. Directory bootstrap is usually under a minute; the onion is ready once a probe connect works (combined Arti status may still say Bootstrapping). Fail closed at 360s. Data lives in `ONIONWIRE_HOME` if set, otherwise `~/.local/share/onionwire/` (`onionwire.db` + Arti state, mode 0700). First open creates an ed25519 identity key. That key **is** you.
|
||||
|
||||
Then `F2` to share your invite, `F3` to paste a friend’s. Mouse-select the `onionwire:v1:…` line to copy.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,4 +13,7 @@ mod store;
|
|||
pub mod tui;
|
||||
pub mod wallet;
|
||||
|
||||
pub use store::{Friend, FriendProfile, Message, Payment, PaymentWrite, SelfIdentity, Store};
|
||||
pub use store::{
|
||||
Friend, FriendProfile, Message, Payment, PaymentWrite, SelfIdentity, Store,
|
||||
resolve_store_passphrase,
|
||||
};
|
||||
|
|
|
|||
23
src/main.rs
23
src/main.rs
|
|
@ -1,5 +1,4 @@
|
|||
use onionwire::tui::AppExit;
|
||||
use std::io::{self, Write};
|
||||
|
||||
fn print_help() {
|
||||
let v = env!("CARGO_PKG_VERSION");
|
||||
|
|
@ -35,7 +34,8 @@ async fn main() {
|
|||
|
||||
async fn boot() -> Result<(), String> {
|
||||
let home = onionwire::Store::home_dir().map_err(|e| e.to_string())?;
|
||||
let pass = store_passphrase()?;
|
||||
let pass = onionwire::resolve_store_passphrase(|p| rpassword::prompt_password(p))
|
||||
.map_err(|e| e.to_string())?;
|
||||
eprintln!("onionwire: bootstrapping Arti…");
|
||||
let node = onionwire::node::Node::start_with_passphrase(home.clone(), &pass).await?;
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
|
|
@ -48,22 +48,3 @@ async fn boot() -> Result<(), String> {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn store_passphrase() -> Result<String, String> {
|
||||
match std::env::var("ONIONWIRE_STORE_PASSPHRASE") {
|
||||
Ok(p) if p.is_empty() => Err("empty passphrase".into()),
|
||||
Ok(p) => Ok(p),
|
||||
Err(_) => {
|
||||
eprint!("onionwire: store passphrase: ");
|
||||
let _ = io::stderr().flush();
|
||||
let mut s = String::new();
|
||||
io::stdin().read_line(&mut s).map_err(|e| e.to_string())?;
|
||||
let s = s.trim_end_matches(['\n', '\r']).to_string();
|
||||
if s.is_empty() {
|
||||
Err("empty passphrase".into())
|
||||
} else {
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
src/store.rs
24
src/store.rs
|
|
@ -4,9 +4,9 @@ use std::path::{Path, PathBuf};
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use rand::rngs::OsRng;
|
||||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
use x25519_dalek::{PublicKey as X25519Public, StaticSecret};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
|
@ -831,6 +831,26 @@ fn passphrase_from_env() -> Result<String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Env `ONIONWIRE_STORE_PASSPHRASE` if set (non-empty). Otherwise `read_secret`
|
||||
/// (TTY, no echo). Empty values fail closed.
|
||||
pub fn resolve_store_passphrase(
|
||||
read_secret: impl FnOnce(&str) -> std::io::Result<String>,
|
||||
) -> Result<String> {
|
||||
match std::env::var("ONIONWIRE_STORE_PASSPHRASE") {
|
||||
Ok(p) if p.is_empty() => Err(Error("empty passphrase".into())),
|
||||
Ok(p) => Ok(p),
|
||||
Err(_) => {
|
||||
let s = read_secret("onionwire: store passphrase: ")?;
|
||||
let s = s.trim_end_matches(['\n', '\r']).to_string();
|
||||
if s.is_empty() {
|
||||
Err(Error("empty passphrase".into()))
|
||||
} else {
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mkdir_700(path: &Path) -> Result<()> {
|
||||
fs::create_dir_all(path)?;
|
||||
let mut perms = fs::metadata(path)?.permissions();
|
||||
|
|
|
|||
|
|
@ -264,3 +264,64 @@ fn reopen_with_same_passphrase_decrypts() {
|
|||
let msgs = store.list_messages(&pk(1)).unwrap();
|
||||
assert_eq!(msgs[0].plaintext, b"hello again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_passphrase_skips_prompt() {
|
||||
let _g = ENV_LOCK.lock().expect("env lock");
|
||||
unsafe {
|
||||
std::env::set_var("ONIONWIRE_STORE_PASSPHRASE", "from-env");
|
||||
}
|
||||
let mut prompted = false;
|
||||
let got = onionwire::resolve_store_passphrase(|_| {
|
||||
prompted = true;
|
||||
Ok("from-tty".into())
|
||||
});
|
||||
unsafe {
|
||||
std::env::remove_var("ONIONWIRE_STORE_PASSPHRASE");
|
||||
}
|
||||
assert_eq!(got.unwrap(), "from-env");
|
||||
assert!(!prompted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_env_passphrase_is_rejected_without_prompt() {
|
||||
let _g = ENV_LOCK.lock().expect("env lock");
|
||||
unsafe {
|
||||
std::env::set_var("ONIONWIRE_STORE_PASSPHRASE", "");
|
||||
}
|
||||
let mut prompted = false;
|
||||
let err = onionwire::resolve_store_passphrase(|_| {
|
||||
prompted = true;
|
||||
Ok("from-tty".into())
|
||||
})
|
||||
.unwrap_err();
|
||||
unsafe {
|
||||
std::env::remove_var("ONIONWIRE_STORE_PASSPHRASE");
|
||||
}
|
||||
assert!(err.to_string().contains("empty"), "got {err}");
|
||||
assert!(!prompted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_env_reads_secret_and_strips_newline() {
|
||||
let _g = ENV_LOCK.lock().expect("env lock");
|
||||
unsafe {
|
||||
std::env::remove_var("ONIONWIRE_STORE_PASSPHRASE");
|
||||
}
|
||||
let got = onionwire::resolve_store_passphrase(|prompt| {
|
||||
assert!(prompt.contains("store passphrase"), "prompt {prompt}");
|
||||
Ok("secret-from-tty\n".into())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(got, "secret-from-tty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_env_empty_secret_is_rejected() {
|
||||
let _g = ENV_LOCK.lock().expect("env lock");
|
||||
unsafe {
|
||||
std::env::remove_var("ONIONWIRE_STORE_PASSPHRASE");
|
||||
}
|
||||
let err = onionwire::resolve_store_passphrase(|_| Ok(String::new())).unwrap_err();
|
||||
assert!(err.to_string().contains("empty"), "got {err}");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue