Merge pull request 'fix(cli): hide store passphrase like a password prompt' (#14) from feat/hidden-store-passphrase into main
All checks were successful
ci / test (push) Successful in 3m32s
All checks were successful
ci / test (push) Successful in 3m32s
Reviewed-on: #14
This commit is contained in:
commit
389d8f16c4
7 changed files with 113 additions and 25 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
|
@ -2639,6 +2639,7 @@ dependencies = [
|
||||||
"md-5",
|
"md-5",
|
||||||
"rand 0.8.8",
|
"rand 0.8.8",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"rpassword",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"safelog",
|
"safelog",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -3381,6 +3382,17 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"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]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.10"
|
version = "0.9.10"
|
||||||
|
|
@ -3402,6 +3414,16 @@ dependencies = [
|
||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rusqlite"
|
||||||
version = "0.36.0"
|
version = "0.36.0"
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ argon2 = "0.5"
|
||||||
chacha20poly1305 = "0.10"
|
chacha20poly1305 = "0.10"
|
||||||
sha3 = "0.10"
|
sha3 = "0.10"
|
||||||
md-5 = "0.10"
|
md-5 = "0.10"
|
||||||
|
rpassword = "7"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ onionwire
|
||||||
|
|
||||||
or `cargo run --release`.
|
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.
|
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 tui;
|
||||||
pub mod wallet;
|
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 onionwire::tui::AppExit;
|
||||||
use std::io::{self, Write};
|
|
||||||
|
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
let v = env!("CARGO_PKG_VERSION");
|
let v = env!("CARGO_PKG_VERSION");
|
||||||
|
|
@ -35,7 +34,8 @@ async fn main() {
|
||||||
|
|
||||||
async fn boot() -> Result<(), String> {
|
async fn boot() -> Result<(), String> {
|
||||||
let home = onionwire::Store::home_dir().map_err(|e| e.to_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…");
|
eprintln!("onionwire: bootstrapping Arti…");
|
||||||
let node = onionwire::node::Node::start_with_passphrase(home.clone(), &pass).await?;
|
let node = onionwire::node::Node::start_with_passphrase(home.clone(), &pass).await?;
|
||||||
let handle = tokio::runtime::Handle::current();
|
let handle = tokio::runtime::Handle::current();
|
||||||
|
|
@ -48,22 +48,3 @@ async fn boot() -> Result<(), String> {
|
||||||
}
|
}
|
||||||
Ok(())
|
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 std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
use rand::rngs::OsRng;
|
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use rusqlite::{params, Connection, OptionalExtension};
|
use rand::rngs::OsRng;
|
||||||
|
use rusqlite::{Connection, OptionalExtension, params};
|
||||||
use x25519_dalek::{PublicKey as X25519Public, StaticSecret};
|
use x25519_dalek::{PublicKey as X25519Public, StaticSecret};
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
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<()> {
|
fn mkdir_700(path: &Path) -> Result<()> {
|
||||||
fs::create_dir_all(path)?;
|
fs::create_dir_all(path)?;
|
||||||
let mut perms = fs::metadata(path)?.permissions();
|
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();
|
let msgs = store.list_messages(&pk(1)).unwrap();
|
||||||
assert_eq!(msgs[0].plaintext, b"hello again");
|
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