This also adds some support for caching user secrets where appropriate, so it should make interaction with remotes have less friction.
DWSAYGVEOR4D2EKIICEZUWCRGJTUXQQLOUWMYIFV7XN62K44F4FAC
MU6P2JXG6RQEUOUV7HIZNA2734BGUCYMJYXAOLOO2CSGMNKVIH5QC
C267PHOH3QJBSBEWQB3J7PPOOXIUKM3DIIZIPLHPU4D5OXRCGLZAC
NAUECZW353R5RHT4GGQJIEZPA5EYRGQYTSP7IJNBJS3CXBSTNJDQC
QL6K2ZM35B3NIXEMMCJWUSFXOBQHAGXRDMO7ID5DCKTJH4QJVY7QC
RUBBHYZ7MCLKJIHZ3EWEC3JR3FSKOU4T2NH7KRBG7ECAU4JF3LUAC
SMMBFECLGSUKRZW5YPOQPOQCOY2CH2OTZXBSZ3KG2N3J3HQZ5PSAC
EEBKW7VTILH6AGGV57ZIJ3DJGYHDSYBWGU3C7Q4WWAKSVNUGIYMQC
A3RM526Y7LUXNYW4TL56YKQ5GVOK2R5D7JJVTSQ6TT5MEXIR6YAAC
SXEYMYF7P4RZMZ46WPL4IZUTSQ2ATBWYZX7QNVMS3SGOYXYOHAGQC
TFPETWTVADLG2DL7WERHJPGMJVOY4WOKCRWB3NZ3YOOQ4CVAUHBAC
I52XSRUH5RVHQBFWVMAQPTUSPAJ4KNVID2RMI3UGCVKFLYUO6WZAC
L4JXJHWXYNCL4QGJXNKKTOKKTAXKKXBJUUY7HFZGEUZ5A2V5H34QC
CCLLB7OIFNFYJZTG3UCI7536TOCWSCSXR67VELSB466R24WLJSDAC
LJFJEX43HDS33O5HCRXH7AR3GTQZDHNWHEQBOERDNPNXR3B3XZ3QC
IIV3EL2XYI2X7HZWKXEXQFAE3R3KC2Q7SGOT3Q332HSENMYVF32QC
E7UUQQCCX2WSVOSMO4OWCJFFU7RGQKQ4TRBBICVM52K7ATTHYNSAC
6F6AAHK4M2IVS23TVISR5OJSTZXUSEKLOP5BMM7SUHYG2FQNTSGQC
OU6JOR3CDZTH2H3NTGMV3WDIAWPD3VEJI7JRY3VJ7LPDR3QOA52QC
6U42MTEZTINWUU2KLJLQW33ZBZXKTHPTF6TMEG56TO642CQDZQMQC
UDHP4ZVBQZT2VBURB2MDCU2IZDNMCAFSIUKWRBDQ5BWMFKSN2LYQC
TPEH2XNBS5RO4IEVKENVF6P65AH7IX64KK2JAYMSJT3J5GXO67EAC
I6DVZEFUMGH6BFOLGBPM6J4PL5I4PAAODJYG7REXYPDHPKPBLDTAC
SZWBLWZ4LUJZHTSYOIGJMXM7KPCGWJFPKLHYA5MHY7UTPNLZV5KQC
RRCSHAYZ6RLWVPNYHF2F5FSRL2KKJNPQUQRIJYLJGB23BZQQ7JLQC
HKA66XOQ5LOF3E5DIQGVKPEZGZKCXLLYRVTKDK7SAD7Y5JZB5OUQC
} else if let Some(mut dir) = crate::config::global_config_dir() {
dir.push("publickey.json");
if let Ok(key) = std::fs::File::open(&dir) {
let k: libpijul::key::PublicKey = serde_json::from_reader(key).unwrap();
b.insert("key".to_string(), k.key);
} else {
bail!("No identity configured yet. Please use `pijul key` to create one")
}
} else if let Some(_dir) = crate::config::global_config_dir() {
let k = crate::identity::public_key(&crate::identity::choose_identity_name(false).await?)?;
b.insert("key".to_string(), k.key);
} else if let Some(mut dir) = crate::config::global_config_dir() {
dir.push("publickey.json");
if let Ok(key) = std::fs::File::open(&dir) {
let k: libpijul::key::PublicKey = serde_json::from_reader(key).unwrap();
b.insert("key".to_string(), k.key);
} else {
bail!("No identity configured yet. Please use `pijul key` to create one")
}
} else {
let public_key = crate::identity::public_key(
&self
.identity
.clone()
.unwrap_or(crate::identity::choose_identity_name(false).await?),
);
b.insert("key".to_string(), public_key?.key);
fn get_public_key() -> Result<libpijul::key::PublicKey, anyhow::Error> {
if let Some(mut dir) = crate::config::global_config_dir() {
dir.push("publickey.json");
if let Ok(mut pkf) = std::fs::File::open(&dir) {
if let Ok(pkf) = serde_json::from_reader(&mut pkf) {
return Ok(pkf);
}
}
}
bail!("No public key found")
}
let public_key: libpijul::key::PublicKey = if let Ok(pk) = get_public_key() {
pk
} else {
return Ok(());
};
if !done.insert(public_key.key.clone()) {
return Ok(());
}
if let Ok((config, last_modified)) = crate::config::Global::load() {
serde_json::to_writer(
&mut o,
&crate::Identity {
public_key,
email: config.author.email,
name: config.author.full_name,
login: config.author.name,
origin: String::new(),
last_modified,
},
)
.unwrap();
writeln!(o)?;
} else {
debug!("no global config");
}
warn!("Skipping serializing old public key format.");
return Ok(());
fn load_key() -> Result<(libpijul::key::SecretKey, libpijul::key::SKey), anyhow::Error> {
if let Some(mut dir) = crate::config::global_config_dir() {
dir.push("secretkey.json");
if let Ok(key) = std::fs::File::open(&dir) {
let k: libpijul::key::SecretKey = serde_json::from_reader(key)?;
let pass = if k.encryption.is_some() {
Some(rpassword::read_password_from_tty(Some(&format!(
"Password for {:?}: ",
dir
)))?)
} else {
None
};
let sk = k.load(pass.as_deref())?;
Ok((k, sk))
} else {
bail!("Secret key not found, please use `pijul key generate` and try again")
}
} else {
bail!("Secret key not found, please use `pijul key generate` and try again")
}
}
debug!("{:?} {:?}", global_id_path, id);
if let Some(ref mut global_id_path) = global_id_path {
if id.is_none() {
global_id_path.push(e.key());
debug!("{:?}", global_id_path);
if let Ok(f) = std::fs::File::open(&global_id_path) {
if let Ok(id_) = serde_json::from_reader(f) {
id = Some(id_)
} else {
debug!("wrong identity for {:?}", e.key());
}
debug!("{:?}", id);
if let Ok(identities) = crate::identity::Complete::load_all() {
for identity in identities {
if &identity.identity.public_key.key == e.key() {
id = Some(identity.identity);
self.auth_pk(&mut h, &mut key_path).await || self.auth_password(&mut h).await?
let mut stderr = std::io::stderr();
writeln!(stderr, "Warning: Unable to automatically authenticate with server. Please make sure your SSH keys have been uploaded to the Nest.")?;
writeln!(stderr, "For more information, please visit https://pijul.org/manual/the_nest/public_keys.html#ssh-public-keys")?;
self.auth_password(&mut h).await?
}
async fn auth_pk(
&self,
h: &mut thrussh::client::Handle<SshClient>,
key_path: &mut PathBuf,
) -> bool {
if h.is_closed() {
return false;
}
let mut authenticated = false;
let mut keys = Vec::new();
if let Some(ref file) = self.config.identity_file {
keys.push(file.as_str())
} else {
keys.push("id_ed25519");
keys.push("id_rsa");
}
for k in keys.iter() {
key_path.push(k);
let k = if let Some(k) = load_secret_key(&key_path, k) {
k
} else {
key_path.pop();
continue;
};
if let Ok(auth) = h
.authenticate_publickey(&self.config.user, Arc::new(k))
.await
{
authenticated = auth
}
key_path.pop();
if authenticated {
return true;
}
}
false
let pass = rpassword::read_password_from_tty(Some(&format!(
"Password for {}@{}: ",
self.config.user, self.config.host_name
)))?;
h.authenticate_password(self.config.user.to_string(), &pass)
.await
}
}
pub fn load_secret_key<P: AsRef<Path>>(key_path: P, k: &str) -> Option<thrussh_keys::key::KeyPair> {
match thrussh_keys::load_secret_key(key_path.as_ref(), None) {
Ok(k) => Some(k),
Err(e) => {
if let thrussh_keys::Error::KeyIsEncrypted = e {
let pass = if let Ok(pass) =
rpassword::read_password_from_tty(Some(&format!("Password for key {:?}: ", k)))
{
pass
} else {
return None;
};
if pass.is_empty() {
return None;
}
if let Ok(k) = thrussh_keys::load_secret_key(&key_path, Some(&pass)) {
return Some(k);
// Authentication can be attempted multiple times
let mut authenticated = false;
let username = format!("{}@{}", self.config.user, self.config.host_name);
// Try authenticate using the user's keyring
if let Ok(password) = keyring::Entry::new("pijul", &username).get_password() {
authenticated = h
.authenticate_password(self.config.user.to_string(), &password)
.await?;
}
// Try authenticate using user's password
if !authenticated {
let password = dialoguer::Password::with_theme(
crate::config::load_theme()
.expect("Could not load config")
.as_ref(),
)
.with_prompt(format!("Password for {username}"))
.allow_empty_password(true)
.interact()
.unwrap();
authenticated = h
.authenticate_password(self.config.user.to_string(), &password)
.await?;
// If the new password is valid, update the keyring to match
if authenticated {
match keyring::Entry::new("pijul", &username).set_password(&password) {
Err(e) => writeln!(
std::io::stderr(),
"Warning: could not write new password to keychain: {e}"
)
.unwrap(),
_ => (),