The identity wizard implemented here is a simple prototype of what will (hopefully) be a better way to manage keys in Pijul. For more information please see the Zulip discussion at https://pijul.zulipchat.com/#narrow/stream/270693-general/topic/Identity.20management
574FXSXKJF6OVHKHGIFKRFNJOR6URJ23JSQQ47YGMN7XGRXDCSGAC
7ZROQSSN2M3LW6ASYMM6DPR5AERWV4K4TKWKEBKTCEJPMIJAHHXQC
OFQY3GUUXYY5GTLHRH4NSZMLVXJRO67QK3TVDLNNOHZ7T66ZSRJAC
EEBKW7VTILH6AGGV57ZIJ3DJGYHDSYBWGU3C7Q4WWAKSVNUGIYMQC
OKE6SXPP34GKAXKZTWLNHRJRQQN32T3SQCSOWWC3GTV425ZF5Q6QC
SXEYMYF7P4RZMZ46WPL4IZUTSQ2ATBWYZX7QNVMS3SGOYXYOHAGQC
SNZ3OAMCPUGFYON5SZHQQQK46ZZMVMJECJYEUCMG657UVLY2PNBQC
RUBBHYZ7MCLKJIHZ3EWEC3JR3FSKOU4T2NH7KRBG7ECAU4JF3LUAC
QL6K2ZM35B3NIXEMMCJWUSFXOBQHAGXRDMO7ID5DCKTJH4QJVY7QC
IIV3EL2XYI2X7HZWKXEXQFAE3R3KC2Q7SGOT3Q332HSENMYVF32QC
2K7JLB4Z7BS5VFNWD4DO3MKYU7VNPA5MTVHVSDI3FQZ5ICM6XM6QC
QJXNUQFJOAPQT3GUXRDTVKMJZCKFONSXUZMAZB7VC7OHDCGAVCOQC
V4T4SC7OL6WEZNV4XSFBSXY5HPB7VXPSXWSK4Z63QXKQD4JSFNCQC
/// Key generation and management
///
/// Pijul keys are separate from a user's SSH keys. More information
/// can be found in the `Keys` section of the manual.
Key(Key),
/// Identity management
///
/// This is an experimental, interactive way of managing identities
/// with an aim of simplifying the user experience
IdentityWizard(IdentityWizard),
use std::io::Write;
use clap::Parser;
use dialoguer::{Confirm, Input};
use lazy_static::lazy_static;
use regex::Regex;
use crate::key;
const IDENTITY_MESSAGE: &str = "It doesn't look like you have any keys configured!
Each author in Pijul is identified by a unique key to provide greater security & flexibility over names/emails.
To make sure humans (including you!) can easily identify these keys, we need a few personal details.
For more information see https://pijul.org/manual/keys.html";
lazy_static! {
// Email regex taken from https://emailregex.com/
static ref EMAIL: Regex = Regex::new(
r#"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"#
).unwrap();
}
struct RemoteAccount {
user_name: String,
remote_url: String,
}
struct IdentityConfig {
identity_name: String,
full_name: String,
email: Option<String>,
remote_account: Option<RemoteAccount>,
}
/// A representation of the state of the user's current identities
#[derive(Debug)]
struct IdentityStateOnDisk {
/// The configuration directory itself
config_dir: bool,
/// The file secretkey.json
secret_key: bool,
/// The file publickey.json
public_key: bool,
/// At least one identity exists in the identies directory
one_identity: bool,
}
impl IdentityStateOnDisk {
fn has_keys(&self) -> bool {
self.secret_key && self.public_key
}
fn full_identity(&self) -> bool {
self.has_keys() && self.one_identity
}
}
/// Test if all the required configuration files exist
fn get_identities() -> Result<IdentityStateOnDisk, anyhow::Error> {
let config_dir = crate::config::global_config_dir().unwrap();
let identities_dir = config_dir.join("identities").exists();
Ok(IdentityStateOnDisk {
config_dir: config_dir.exists(),
secret_key: config_dir.join("secretkey.json").exists(),
public_key: config_dir.join("publickey.json").exists(),
one_identity: if identities_dir {
config_dir.join("identities").read_dir()?.next().is_some()
} else {
false
},
})
}
fn create_identity() -> Result<IdentityConfig, anyhow::Error> {
let mut stderr = std::io::stderr();
writeln!(stderr, "{IDENTITY_MESSAGE}")?;
let identity_name: String = Input::new()
.with_prompt("Identity name")
.default(whoami::username())
.interact_text()?;
let full_name: String = Input::new()
.with_prompt("Full name")
.default(whoami::realname())
.interact_text()?;
let user_email: String = Input::new()
.with_prompt("Email (leave blank for none)")
.allow_empty(true)
.validate_with(move |input: &String| -> Result<(), &str> {
if input.is_empty() {
return Ok(());
}
if EMAIL.is_match(&input) {
Ok(())
} else {
Err("Invalid email address")
}
})
.interact_text()?;
let email: Option<String> = if user_email.len() > 0 {
Some(user_email)
} else {
None
};
let link_remote: bool = Confirm::new()
.with_prompt("Do you want to link this key to a remote?")
.default(true)
.interact()?;
let remote_account: Option<RemoteAccount> = if link_remote {
Some(RemoteAccount {
user_name: Input::new()
.with_prompt("Remote username")
.default(identity_name.clone())
.interact_text()?,
remote_url: Input::new()
.with_prompt("Remote URL")
.default(String::from("ssh.pijul.com"))
.interact_text()?,
})
} else {
None
};
Ok(IdentityConfig {
identity_name,
full_name,
email,
remote_account,
})
}
#[derive(Parser, Debug)]
pub struct IdentityWizard {}
impl IdentityWizard {
pub async fn run(self) -> Result<(), anyhow::Error> {
// Check the user's filesystem to figure out if any identities exist
let identity_state = get_identities()?;
let mut stderr = std::io::stderr();
if !identity_state.full_identity() {
// User has not set up an identity
let identity = create_identity()?;
writeln!(stderr, "Generating key pair with name: {}", identity.identity_name)?;
key::generate(identity.email, identity.identity_name)?;
if let Some(account) = identity.remote_account {
let remote = format!("{}@{}", account.user_name, account.remote_url);
writeln!(stderr, "Linking identity with remote: {remote}")?;
key::prove(false, remote).await?;
}
} else {
let mut stderr = std::io::stderr();
writeln!(stderr, "There is nothing to do")?;
}
Ok(())
}
}
name = "console"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"regex",
"terminal_size",
"unicode-width",
"winapi",
]
[[package]]