cli: Make key encryption optional
By leaving the passphrase prompt blank, users can create unencrypted keys.
This commit is contained in:
parent
e593191ae2
commit
f92308cf25
|
|
@ -1,6 +1,7 @@
|
|||
#![allow(clippy::or_fun_call)]
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Not as _;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
|
|
@ -109,22 +110,25 @@ pub fn init(options: Options) -> anyhow::Result<()> {
|
|||
} else {
|
||||
term::passphrase_confirm("Enter a passphrase:", RAD_PASSPHRASE)
|
||||
}?;
|
||||
let passphrase = passphrase.trim().is_empty().not().then_some(passphrase);
|
||||
let spinner = term::spinner("Creating your Ed25519 keypair...");
|
||||
let profile = Profile::init(home, alias, passphrase.clone())?;
|
||||
spinner.finish();
|
||||
|
||||
match ssh::agent::Agent::connect() {
|
||||
Ok(mut agent) => {
|
||||
let mut spinner = term::spinner("Adding your radicle key to ssh-agent...");
|
||||
if register(&mut agent, &profile, passphrase).is_ok() {
|
||||
spinner.finish();
|
||||
} else {
|
||||
spinner.message("Could not register radicle key in ssh-agent.");
|
||||
spinner.warn();
|
||||
if let Some(passphrase) = passphrase {
|
||||
match ssh::agent::Agent::connect() {
|
||||
Ok(mut agent) => {
|
||||
let mut spinner = term::spinner("Adding your radicle key to ssh-agent...");
|
||||
if register(&mut agent, &profile, passphrase).is_ok() {
|
||||
spinner.finish();
|
||||
} else {
|
||||
spinner.message("Could not register radicle key in ssh-agent.");
|
||||
spinner.warn();
|
||||
}
|
||||
}
|
||||
Err(e) if e.is_not_running() => {}
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
Err(e) if e.is_not_running() => {}
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
|
||||
term::success!(
|
||||
|
|
@ -144,7 +148,13 @@ pub fn init(options: Options) -> anyhow::Result<()> {
|
|||
/// Try loading the identity's key into SSH Agent, falling back to verifying `RAD_PASSPHRASE` for
|
||||
/// use.
|
||||
pub fn authenticate(options: Options, profile: &Profile) -> anyhow::Result<()> {
|
||||
// Authenticate with SSH Agent only if it is running.
|
||||
if !profile.keystore.is_encrypted()? {
|
||||
term::success!("Authenticated as {}", term::format::tertiary(profile.id()));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If our key is encrypted, we try to authenticate with SSH Agent and
|
||||
// register it; only if it is running.
|
||||
match ssh::agent::Agent::connect() {
|
||||
Ok(mut agent) => {
|
||||
if agent.request_identities()?.contains(&profile.public_key) {
|
||||
|
|
@ -174,7 +184,7 @@ pub fn authenticate(options: Options, profile: &Profile) -> anyhow::Result<()> {
|
|||
|
||||
// Try RAD_PASSPHRASE fallback.
|
||||
if let Some(passphrase) = profile::env::passphrase() {
|
||||
ssh::keystore::MemorySigner::load(&profile.keystore, passphrase)
|
||||
ssh::keystore::MemorySigner::load(&profile.keystore, Some(passphrase))
|
||||
.map_err(|_| anyhow!("RAD_PASSPHRASE failed"))?;
|
||||
return Ok(());
|
||||
};
|
||||
|
|
@ -191,7 +201,7 @@ pub fn register(
|
|||
) -> anyhow::Result<()> {
|
||||
let secret = profile
|
||||
.keystore
|
||||
.secret_key(passphrase)
|
||||
.secret_key(Some(passphrase))
|
||||
.map_err(|e| {
|
||||
if e.is_crypto_err() {
|
||||
anyhow!("could not decrypt secret key: invalid passphrase")
|
||||
|
|
|
|||
|
|
@ -24,10 +24,16 @@ pub fn start(
|
|||
term::success!("Node is already running");
|
||||
return Ok(());
|
||||
}
|
||||
// Ask passphrase here, otherwise it'll be a fatal error when running the daemon
|
||||
// without `RAD_PASSPHRASE`. To keep things consistent, we also use this in foreground mode.
|
||||
let passphrase = term::io::passphrase(profile::env::RAD_PASSPHRASE)
|
||||
.context(format!("`{}` must be set", profile::env::RAD_PASSPHRASE))?;
|
||||
let envs = if profile.keystore.is_encrypted()? {
|
||||
// Ask passphrase here, otherwise it'll be a fatal error when running the daemon
|
||||
// without `RAD_PASSPHRASE`. To keep things consistent, we also use this in foreground mode.
|
||||
let passphrase = term::io::passphrase(profile::env::RAD_PASSPHRASE)
|
||||
.context(format!("`{}` must be set", profile::env::RAD_PASSPHRASE))?;
|
||||
|
||||
Some((profile::env::RAD_PASSPHRASE, passphrase))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Since we checked that the node is not running, it's safe to use `--force`
|
||||
// here.
|
||||
|
|
@ -42,7 +48,7 @@ pub fn start(
|
|||
|
||||
process::Command::new("radicle-node")
|
||||
.args(options)
|
||||
.env(profile::env::RAD_PASSPHRASE, passphrase)
|
||||
.envs(envs)
|
||||
.stdin(process::Stdio::null())
|
||||
.stdout(process::Stdio::from(log))
|
||||
.stderr(process::Stdio::null())
|
||||
|
|
@ -52,7 +58,7 @@ pub fn start(
|
|||
} else {
|
||||
let mut child = process::Command::new("radicle-node")
|
||||
.args(options)
|
||||
.env(profile::env::RAD_PASSPHRASE, passphrase)
|
||||
.envs(envs)
|
||||
.spawn()?;
|
||||
|
||||
child.wait()?;
|
||||
|
|
@ -139,6 +145,7 @@ pub fn status(node: &Node, profile: &Profile) -> anyhow::Result<()> {
|
|||
term::success!("Node is {}", term::format::positive("running"));
|
||||
} else {
|
||||
term::info!("Node is {}", term::format::negative("stopped"));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let sessions = sessions(node)?;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
|
|||
}
|
||||
let passphrase = passphrase(RAD_PASSPHRASE)?;
|
||||
let spinner = spinner("Unsealing key...");
|
||||
let signer = MemorySigner::load(&profile.keystore, passphrase)?;
|
||||
let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?;
|
||||
|
||||
spinner.finish();
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ ssh = ["radicle-ssh", "ssh-key"]
|
|||
|
||||
[dependencies]
|
||||
amplify = { version = "4.0.0" }
|
||||
cyphernet = { version = "0.2.0", optional = true }
|
||||
cyphernet = { version = "0.2.0", optional = true, features = ["ed25519"] }
|
||||
multibase = { version = "0.9.1" }
|
||||
ec25519 = { version = "0.1.0", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
|
|||
|
|
@ -56,11 +56,9 @@ impl Keystore {
|
|||
///
|
||||
/// The `comment` is associated with the private key.
|
||||
/// The `passphrase` is used to encrypt the private key.
|
||||
pub fn init(
|
||||
&self,
|
||||
comment: &str,
|
||||
passphrase: impl Into<Passphrase>,
|
||||
) -> Result<PublicKey, Error> {
|
||||
///
|
||||
/// If `passphrase` is `None`, the key is not encrypted.
|
||||
pub fn init(&self, comment: &str, passphrase: Option<Passphrase>) -> Result<PublicKey, Error> {
|
||||
self.store(keypair::generate(), comment, passphrase)
|
||||
}
|
||||
|
||||
|
|
@ -69,12 +67,16 @@ impl Keystore {
|
|||
&self,
|
||||
keypair: KeyPair,
|
||||
comment: &str,
|
||||
passphrase: impl Into<Passphrase>,
|
||||
passphrase: Option<Passphrase>,
|
||||
) -> Result<PublicKey, Error> {
|
||||
let ssh_pair = ssh_key::private::Ed25519Keypair::from_bytes(&keypair)?;
|
||||
let ssh_pair = ssh_key::private::KeypairData::Ed25519(ssh_pair);
|
||||
let secret = ssh_key::PrivateKey::new(ssh_pair, comment)?;
|
||||
let secret = secret.encrypt(ssh_key::rand_core::OsRng, passphrase.into())?;
|
||||
let secret = if let Some(p) = passphrase {
|
||||
secret.encrypt(ssh_key::rand_core::OsRng, p)?
|
||||
} else {
|
||||
secret
|
||||
};
|
||||
let public = secret.public_key();
|
||||
let path = self.path.join("radicle");
|
||||
|
||||
|
|
@ -111,16 +113,19 @@ impl Keystore {
|
|||
/// Returns `None` if it wasn't found.
|
||||
pub fn secret_key(
|
||||
&self,
|
||||
passphrase: Passphrase,
|
||||
passphrase: Option<Passphrase>,
|
||||
) -> Result<Option<Zeroizing<SecretKey>>, Error> {
|
||||
let path = self.path.join("radicle");
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let encrypted = ssh_key::PrivateKey::read_openssh_file(&path)?;
|
||||
let secret = encrypted.decrypt(passphrase)?;
|
||||
|
||||
let secret = ssh_key::PrivateKey::read_openssh_file(&path)?;
|
||||
let secret = if let Some(p) = passphrase {
|
||||
secret.decrypt(p)?
|
||||
} else {
|
||||
secret
|
||||
};
|
||||
match secret.key_data() {
|
||||
ssh_key::private::KeypairData::Ed25519(pair) => {
|
||||
Ok(Some(SecretKey::from(pair.to_bytes()).into()))
|
||||
|
|
@ -128,6 +133,14 @@ impl Keystore {
|
|||
_ => Err(Error::InvalidKeyType),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the secret key is encrypted.
|
||||
pub fn is_encrypted(&self) -> Result<bool, Error> {
|
||||
let path = self.path.join("radicle");
|
||||
let secret = ssh_key::PrivateKey::read_openssh_file(&path)?;
|
||||
|
||||
Ok(secret.is_encrypted())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
@ -192,7 +205,10 @@ impl Ecdh for MemorySigner {
|
|||
|
||||
impl MemorySigner {
|
||||
/// Load this signer from a keystore, given a secret key passphrase.
|
||||
pub fn load(keystore: &Keystore, passphrase: Passphrase) -> Result<Self, MemorySignerError> {
|
||||
pub fn load(
|
||||
keystore: &Keystore,
|
||||
passphrase: Option<Passphrase>,
|
||||
) -> Result<Self, MemorySignerError> {
|
||||
let public = keystore
|
||||
.public_key()?
|
||||
.ok_or_else(|| MemorySignerError::NotFound(keystore.path().to_path_buf()))?;
|
||||
|
|
@ -238,20 +254,38 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_init() {
|
||||
fn test_init_passphrase() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = Keystore::new(&tmp.path());
|
||||
|
||||
let public = store.init("test", "hunter".to_owned()).unwrap();
|
||||
let public = store
|
||||
.init("test", Some("hunter".to_owned().into()))
|
||||
.unwrap();
|
||||
assert_eq!(public, store.public_key().unwrap().unwrap());
|
||||
assert!(store.is_encrypted().unwrap());
|
||||
|
||||
let secret = store
|
||||
.secret_key("hunter".to_owned().into())
|
||||
.secret_key(Some("hunter".to_owned().into()))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(PublicKey::from(secret.public_key()), public);
|
||||
|
||||
store.secret_key("blunder".to_owned().into()).unwrap_err(); // Wrong passphrase.
|
||||
store
|
||||
.secret_key(Some("blunder".to_owned().into()))
|
||||
.unwrap_err(); // Wrong passphrase.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_init_no_passphrase() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = Keystore::new(&tmp.path());
|
||||
|
||||
let public = store.init("test", None).unwrap();
|
||||
assert_eq!(public, store.public_key().unwrap().unwrap());
|
||||
assert!(!store.is_encrypted().unwrap());
|
||||
|
||||
let secret = store.secret_key(None).unwrap().unwrap();
|
||||
assert_eq!(PublicKey::from(secret.public_key()), public);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -259,8 +293,10 @@ mod tests {
|
|||
let tmp = tempfile::tempdir().unwrap();
|
||||
let store = Keystore::new(&tmp.path());
|
||||
|
||||
let public = store.init("test", "hunter".to_owned()).unwrap();
|
||||
let signer = MemorySigner::load(&store, "hunter".to_owned().into()).unwrap();
|
||||
let public = store
|
||||
.init("test", Some("hunter".to_owned().into()))
|
||||
.unwrap();
|
||||
let signer = MemorySigner::load(&store, Some("hunter".to_owned().into())).unwrap();
|
||||
|
||||
assert_eq!(public, *signer.public_key());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ pub const CONTRIBUTOR_PATCH_ID: &str = "044b577cc7551cd09d4b2f03566a553762180de4
|
|||
pub const CONTRIBUTOR_COMMENT_1: &str = "92aab76516ae7f60a9b2952043ba578383de7d46";
|
||||
pub const CONTRIBUTOR_COMMENT_2: &str = "cb360eee0ec70563d5c4c3613fdc076648523248";
|
||||
|
||||
const PASSWORD: &str = "radicle";
|
||||
|
||||
/// Create a new profile.
|
||||
pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile {
|
||||
let home = Home::new(home).unwrap();
|
||||
|
|
@ -56,9 +54,7 @@ pub fn profile(home: &Path, seed: [u8; 32]) -> radicle::Profile {
|
|||
let keypair = KeyPair::from_seed(Seed::from(seed));
|
||||
|
||||
radicle::storage::git::transport::local::register(storage.clone());
|
||||
keystore
|
||||
.store(keypair.clone(), "radicle", PASSWORD.to_owned())
|
||||
.unwrap();
|
||||
keystore.store(keypair.clone(), "radicle", None).unwrap();
|
||||
|
||||
radicle::Profile {
|
||||
home,
|
||||
|
|
@ -85,7 +81,7 @@ pub fn contributor(dir: &Path) -> Context {
|
|||
|
||||
let home = dir.join("radicle");
|
||||
let profile = profile(home.as_path(), seed);
|
||||
let signer = MemorySigner::load(&profile.keystore, PASSWORD.to_owned().into()).unwrap();
|
||||
let signer = MemorySigner::load(&profile.keystore, None).unwrap();
|
||||
|
||||
seed_with_signer(dir, profile, &signer)
|
||||
}
|
||||
|
|
@ -102,7 +98,6 @@ fn seed_with_signer<G: Signer>(dir: &Path, profile: radicle::Profile, signer: &G
|
|||
let workdir = dir.join("hello-world");
|
||||
|
||||
env::set_var("RAD_COMMIT_TIME", TIMESTAMP.to_string());
|
||||
env::set_var("RAD_PASSPHRASE", PASSWORD);
|
||||
|
||||
fs::create_dir_all(&workdir).unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
use std::{env, fs, net, process};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use anyhow::anyhow;
|
||||
use crossbeam_channel as chan;
|
||||
use cyphernet::addr::PeerAddr;
|
||||
use localtime::LocalDuration;
|
||||
|
||||
use radicle::crypto;
|
||||
use radicle::node;
|
||||
use radicle::prelude::Signer;
|
||||
use radicle::profile;
|
||||
|
|
@ -126,9 +125,7 @@ fn execute() -> anyhow::Result<()> {
|
|||
|
||||
log::info!(target: "node", "Unlocking node keystore..");
|
||||
|
||||
let passphrase = env::var(profile::env::RAD_PASSPHRASE)
|
||||
.map(crypto::ssh::Passphrase::from)
|
||||
.context(format!("`{}` must be set", profile::env::RAD_PASSPHRASE))?;
|
||||
let passphrase = profile::env::passphrase();
|
||||
let keystore = Keystore::new(&home.keys());
|
||||
let signer = MemorySigner::load(&keystore, passphrase)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ impl Environment {
|
|||
/// is required. Use [`Environment::profile`] otherwise.
|
||||
pub fn node(&mut self, config: Config) -> Node<MemorySigner> {
|
||||
let profile = self.profile(&config.alias);
|
||||
let signer = MemorySigner::load(&profile.keystore, "radicle".to_owned().into()).unwrap();
|
||||
let signer = MemorySigner::load(&profile.keystore, None).unwrap();
|
||||
let tracking_db = profile.home.node().join(TRACKING_DB_FILE);
|
||||
TrackingStore::Config::open(tracking_db).unwrap();
|
||||
let addresses_db = profile.home.node().join(ADDRESS_DB_FILE);
|
||||
|
|
@ -109,9 +109,7 @@ impl Environment {
|
|||
Book::open(addresses_db).unwrap();
|
||||
|
||||
transport::local::register(storage.clone());
|
||||
keystore
|
||||
.store(keypair.clone(), "radicle", "radicle".to_owned())
|
||||
.unwrap();
|
||||
keystore.store(keypair.clone(), "radicle", None).unwrap();
|
||||
|
||||
// Ensures that each user has a unique but deterministic public key.
|
||||
self.users += 1;
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ pub fn passphrase_confirm<K: AsRef<OsStr>>(
|
|||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
||||
.with_custom_confirmation_message("Repeat passphrase:")
|
||||
.with_custom_confirmation_error_message("The passphrases don't match.")
|
||||
.with_help_message("This passphrase protects your radicle identity")
|
||||
.with_help_message("Leave this blank to keep your radicle key unencrypted")
|
||||
.prompt()?,
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ fn main() -> anyhow::Result<()> {
|
|||
let passphrase = passphrase.trim().to_owned().into();
|
||||
let secret = profile
|
||||
.keystore
|
||||
.secret_key(passphrase)?
|
||||
.secret_key(Some(passphrase))?
|
||||
.ok_or_else(|| anyhow!("Key not found in {:?}", profile.keystore.path()))?;
|
||||
|
||||
agent.register(&secret)?;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ pub mod env {
|
|||
/// Passphrase for the encrypted radicle secret key.
|
||||
pub const RAD_PASSPHRASE: &str = "RAD_PASSPHRASE";
|
||||
|
||||
/// Get the radicle passphrase from the environment.
|
||||
pub fn passphrase() -> Option<super::Passphrase> {
|
||||
let Ok(passphrase) = std::env::var(RAD_PASSPHRASE) else {
|
||||
return None;
|
||||
|
|
@ -139,11 +140,7 @@ pub struct Profile {
|
|||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn init(
|
||||
home: Home,
|
||||
alias: Alias,
|
||||
passphrase: impl Into<Passphrase>,
|
||||
) -> Result<Self, Error> {
|
||||
pub fn init(home: Home, alias: Alias, passphrase: Option<Passphrase>) -> Result<Self, Error> {
|
||||
let storage = Storage::open(home.storage())?;
|
||||
let keystore = Keystore::new(&home.keys());
|
||||
let public_key = keystore.init("radicle", passphrase)?;
|
||||
|
|
@ -189,8 +186,13 @@ impl Profile {
|
|||
}
|
||||
|
||||
pub fn signer(&self) -> Result<Box<dyn Signer>, Error> {
|
||||
if !self.keystore.is_encrypted()? {
|
||||
let signer = keystore::MemorySigner::load(&self.keystore, None)?;
|
||||
return Ok(signer.boxed());
|
||||
}
|
||||
|
||||
if let Some(passphrase) = env::passphrase() {
|
||||
let signer = keystore::MemorySigner::load(&self.keystore, passphrase)?;
|
||||
let signer = keystore::MemorySigner::load(&self.keystore, Some(passphrase))?;
|
||||
return Ok(signer.boxed());
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue