0.3.0-alpha
This commit is contained in:
474
src/cli.rs
474
src/cli.rs
@@ -2,35 +2,23 @@ use crate::error::*;
|
||||
use crate::luks::{Fido2LuksToken, LuksDevice};
|
||||
use crate::util::sha256;
|
||||
use crate::*;
|
||||
pub use cli_args::Args;
|
||||
use cli_args::*;
|
||||
|
||||
use structopt::clap::Shell;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use ctap::{FidoCredential, FidoErrorKind};
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::io::Write;
|
||||
use std::iter::FromIterator;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::time::SystemTime;
|
||||
use structopt::clap::Shell;
|
||||
use structopt::StructOpt;
|
||||
|
||||
pub use cli_args::Args;
|
||||
use std::iter::FromIterator;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn read_pin(ap: &AuthenticatorParameters) -> Fido2LuksResult<String> {
|
||||
if let Some(src) = ap.pin_source.as_ref() {
|
||||
let mut pin = String::new();
|
||||
File::open(src)?.read_to_string(&mut pin)?;
|
||||
Ok(pin.trim_end_matches("\n").to_string()) //remove trailing newline
|
||||
} else {
|
||||
util::read_password("Authenticator PIN", false)
|
||||
}
|
||||
fn read_pin() -> Fido2LuksResult<String> {
|
||||
util::read_password_tty("Authenticator PIN", false)
|
||||
}
|
||||
|
||||
fn derive_secret(
|
||||
@@ -39,6 +27,9 @@ fn derive_secret(
|
||||
timeout: u64,
|
||||
pin: Option<&str>,
|
||||
) -> Fido2LuksResult<([u8; 32], FidoCredential)> {
|
||||
if credentials.is_empty() {
|
||||
return Err(Fido2LuksError::InsufficientCredentials);
|
||||
}
|
||||
let timeout = Duration::from_secs(timeout);
|
||||
let start = SystemTime::now();
|
||||
|
||||
@@ -74,20 +65,62 @@ pub fn extend_creds_device(
|
||||
creds: &[HexEncoded],
|
||||
luks_dev: &mut LuksDevice,
|
||||
) -> Fido2LuksResult<Vec<HexEncoded>> {
|
||||
let mut additional = HashSet::from_iter(creds.iter().cloned());
|
||||
let mut additional = HashSet::new();
|
||||
additional.extend(creds.iter().cloned());
|
||||
for token in luks_dev.tokens()? {
|
||||
for cred in token?.1.credential {
|
||||
let parsed = HexEncoded::from_str(cred.as_str())?;
|
||||
let parsed = HexEncoded::from_str(cred.as_str()).map_err(|_e| {
|
||||
Fido2LuksError::HexEncodingError {
|
||||
string: cred.clone(),
|
||||
}
|
||||
})?;
|
||||
additional.insert(parsed);
|
||||
}
|
||||
}
|
||||
Ok(Vec::from_iter(additional.into_iter()))
|
||||
}
|
||||
|
||||
pub fn read_password_pin_prefixed(
|
||||
reader: impl Fn() -> Fido2LuksResult<String>,
|
||||
pub fn get_input(
|
||||
secret: &SecretParameters,
|
||||
authenticator: &AuthenticatorParameters,
|
||||
interactive: bool,
|
||||
q: &str,
|
||||
verify: bool,
|
||||
) -> Fido2LuksResult<(Option<String>, [u8; 32])> {
|
||||
let read = reader()?;
|
||||
let password_helper = secret
|
||||
.password_helper
|
||||
.as_ref()
|
||||
.map(|helper| move || helper.obtain());
|
||||
let salt = &secret.salt;
|
||||
Ok(if interactive {
|
||||
(
|
||||
if authenticator.pin && may_require_pin()? {
|
||||
Some(read_pin()?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
salt.obtain_sha256(Some(|| util::read_password_tty(q, verify)))?,
|
||||
)
|
||||
} else {
|
||||
match (
|
||||
authenticator.pin && may_require_pin()?,
|
||||
authenticator.pin_prefixed,
|
||||
) {
|
||||
(true, false) => (Some(read_pin()?), salt.obtain_sha256(password_helper)?),
|
||||
(true, true) => read_password_pin_prefixed(|| {
|
||||
salt.obtain(password_helper).and_then(|secret| {
|
||||
String::from_utf8(secret).map_err(|e| Fido2LuksError::from(e))
|
||||
})
|
||||
})?,
|
||||
(false, _) => (None, salt.obtain_sha256(password_helper)?),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_password_pin_prefixed(
|
||||
prefixed: impl Fn() -> Fido2LuksResult<String>,
|
||||
) -> Fido2LuksResult<(Option<String>, [u8; 32])> {
|
||||
let read = prefixed()?;
|
||||
let separator = ':';
|
||||
let mut parts = read.split(separator);
|
||||
let pin = parts.next().filter(|p| p.len() > 0).map(|p| p.to_string());
|
||||
@@ -95,18 +128,47 @@ pub fn read_password_pin_prefixed(
|
||||
Some(ref pin) if read.len() > pin.len() => {
|
||||
read.chars().skip(pin.len() + 1).collect::<String>()
|
||||
}
|
||||
_ => String::new(),
|
||||
Some(_) => String::new(),
|
||||
_ => read
|
||||
.chars()
|
||||
.skip(read.chars().next().map(|c| c == separator).unwrap_or(false) as usize)
|
||||
.collect::<String>(),
|
||||
};
|
||||
Ok((pin, util::sha256(&[password.as_bytes()])))
|
||||
}
|
||||
|
||||
/// generate an more readable name from common paths
|
||||
pub fn derive_credential_name(path: &Path) -> String {
|
||||
match path.file_name() {
|
||||
Some(name)
|
||||
if path
|
||||
.iter()
|
||||
.any(|p| p == "by-label" || p == "by-partlabel" || p == "by-uuid") =>
|
||||
{
|
||||
name.to_string_lossy().as_ref().to_string()
|
||||
}
|
||||
_ => path.display().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_cmdline() -> Args {
|
||||
Args::from_args()
|
||||
}
|
||||
|
||||
pub fn prompt_interaction(interactive: bool) {
|
||||
if interactive {
|
||||
println!("Authorize using your FIDO device");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
let mut stdout = io::stdout();
|
||||
let args = parse_cmdline();
|
||||
let log = |message: &dyn Fn() -> String| {
|
||||
if args.verbose {
|
||||
eprintln!("{}", &*message());
|
||||
}
|
||||
};
|
||||
let interactive = args.interactive;
|
||||
match &args.command {
|
||||
Command::Credential {
|
||||
@@ -114,13 +176,13 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
name,
|
||||
} => {
|
||||
let pin_string;
|
||||
let pin = if authenticator.pin {
|
||||
pin_string = read_pin(authenticator)?;
|
||||
let pin = if authenticator.pin && may_require_pin()? {
|
||||
pin_string = read_pin()?;
|
||||
Some(pin_string.as_ref())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cred = make_credential_id(name.as_ref().map(|n| n.as_ref()), pin)?;
|
||||
let cred = make_credential_id(Some(name.as_ref()), pin)?;
|
||||
println!("{}", hex::encode(&cred.id));
|
||||
Ok(())
|
||||
}
|
||||
@@ -131,50 +193,42 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
secret,
|
||||
device,
|
||||
} => {
|
||||
let pin_string;
|
||||
let pin = if authenticator.pin {
|
||||
pin_string = read_pin(authenticator)?;
|
||||
Some(pin_string.as_ref())
|
||||
let (pin, salt) =
|
||||
get_input(&secret, &authenticator, args.interactive, "Password", false)?;
|
||||
let credentials = if let Some(path) = device {
|
||||
let mut dev = LuksDevice::load(path)?;
|
||||
let luks2 = dev.is_luks2()?;
|
||||
log(&|| format!("luks2 supported: {}", luks2));
|
||||
extend_creds_device(
|
||||
credentials
|
||||
.ids
|
||||
.clone()
|
||||
.map(|cs| cs.0)
|
||||
.unwrap_or_default()
|
||||
.as_slice(),
|
||||
&mut dev,
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
credentials.ids.clone().map(|cs| cs.0).unwrap_or_default()
|
||||
};
|
||||
let (pin, salt) = match (
|
||||
&secret.password_helper,
|
||||
authenticator.pin,
|
||||
authenticator.pin_prefixed,
|
||||
args.interactive,
|
||||
) {
|
||||
(Some(phelper), true, true, false) => {
|
||||
read_password_pin_prefixed(|| phelper.obtain())?
|
||||
}
|
||||
(Some(phelper), false, false, false) => (None, secret.salt.obtain_sha256(&phelper)),
|
||||
|
||||
(phelper, pin, _, true) => (
|
||||
if pin {
|
||||
pin_string = read_pin(authenticator)?;
|
||||
Some(pin_string.as_ref())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
match phelper {
|
||||
None | Some(PasswordHelper::Stdin) => {
|
||||
util::read_password_hashed("Password", false)
|
||||
}
|
||||
Some(phelper) => secret.salt.obtain_sha256(&phelper),
|
||||
}?,
|
||||
),
|
||||
};
|
||||
let credentials = if let Some(dev) = device {
|
||||
extend_creds_device(credentials.ids.0.as_slice(), &mut LuksDevice::load(dev)?)?
|
||||
} else {
|
||||
credentials.ids.0
|
||||
};
|
||||
let (secret, _cred) = derive_secret(
|
||||
credentials.as_slice(),
|
||||
log(&|| {
|
||||
format!(
|
||||
"credentials: {}",
|
||||
credentials
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
});
|
||||
prompt_interaction(interactive);
|
||||
let (secret, cred) = derive_secret(
|
||||
&credentials,
|
||||
&salt,
|
||||
authenticator.await_time,
|
||||
pin.as_deref(),
|
||||
)?;
|
||||
log(&|| format!("credential used: {}", hex::encode(&cred.id)));
|
||||
if *binary {
|
||||
stdout.write_all(&secret[..])?;
|
||||
} else {
|
||||
@@ -189,7 +243,6 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
secret,
|
||||
luks_mod,
|
||||
existing_secret: other_secret,
|
||||
auto_credential,
|
||||
..
|
||||
}
|
||||
| Command::ReplaceKey {
|
||||
@@ -199,21 +252,41 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
secret,
|
||||
luks_mod,
|
||||
replacement: other_secret,
|
||||
remove_cred,
|
||||
..
|
||||
} => {
|
||||
let pin = if authenticator.pin {
|
||||
Some(read_pin(authenticator)?)
|
||||
let mut luks_dev = LuksDevice::load(&luks.device)?;
|
||||
|
||||
let luks2 = luks_dev.is_luks2()?;
|
||||
|
||||
log(&|| format!("luks2 supported: {}", luks2));
|
||||
|
||||
let credentials = if !luks.disable_token && luks2 {
|
||||
extend_creds_device(
|
||||
credentials
|
||||
.ids
|
||||
.clone()
|
||||
.map(|cs| cs.0)
|
||||
.unwrap_or_default()
|
||||
.as_slice(),
|
||||
&mut luks_dev,
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
credentials.ids.clone().map(|cs| cs.0).unwrap_or_default()
|
||||
};
|
||||
let salt = |q: &str, verify: bool| -> Fido2LuksResult<[u8; 32]> {
|
||||
if interactive || secret.password_helper == PasswordHelper::Stdin {
|
||||
util::read_password_hashed(q, verify)
|
||||
} else {
|
||||
secret.salt.obtain_sha256(&secret.password_helper)
|
||||
}
|
||||
log(&|| {
|
||||
format!(
|
||||
"credentials: {}",
|
||||
credentials
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
});
|
||||
let inputs = |q: &str, verify: bool| -> Fido2LuksResult<(Option<String>, [u8; 32])> {
|
||||
get_input(&secret, &authenticator, args.interactive, q, verify)
|
||||
};
|
||||
|
||||
let other_secret = |salt_q: &str,
|
||||
verify: bool|
|
||||
-> Fido2LuksResult<(Vec<u8>, Option<FidoCredential>)> {
|
||||
@@ -224,38 +297,70 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
} => Ok((util::read_keyfile(file)?, None)),
|
||||
OtherSecret {
|
||||
fido_device: true, ..
|
||||
} => Ok(derive_secret(
|
||||
&credentials.ids.0,
|
||||
&salt(salt_q, verify)?,
|
||||
authenticator.await_time,
|
||||
pin.as_deref(),
|
||||
)
|
||||
.map(|(secret, cred)| (secret[..].to_vec(), Some(cred)))?),
|
||||
} => {
|
||||
let (pin, salt) = inputs(salt_q, verify)?;
|
||||
prompt_interaction(interactive);
|
||||
Ok(derive_secret(
|
||||
&credentials,
|
||||
&salt,
|
||||
authenticator.await_time,
|
||||
pin.as_deref(),
|
||||
)
|
||||
.map(|(secret, cred)| (secret[..].to_vec(), Some(cred)))?)
|
||||
}
|
||||
_ => Ok((
|
||||
util::read_password(salt_q, verify)?.as_bytes().to_vec(),
|
||||
util::read_password_tty(salt_q, verify)?.as_bytes().to_vec(),
|
||||
None,
|
||||
)),
|
||||
}
|
||||
};
|
||||
let secret = |verify: bool| -> Fido2LuksResult<([u8; 32], FidoCredential)> {
|
||||
derive_secret(
|
||||
&credentials.ids.0,
|
||||
&salt("Password", verify)?,
|
||||
authenticator.await_time,
|
||||
pin.as_deref(),
|
||||
)
|
||||
let secret = |q: &str,
|
||||
verify: bool,
|
||||
credentials: &[HexEncoded]|
|
||||
-> Fido2LuksResult<([u8; 32], FidoCredential)> {
|
||||
let (pin, salt) = inputs(q, verify)?;
|
||||
prompt_interaction(interactive);
|
||||
derive_secret(credentials, &salt, authenticator.await_time, pin.as_deref())
|
||||
};
|
||||
let mut luks_dev = LuksDevice::load(&luks.device)?;
|
||||
// Non overlap
|
||||
match &args.command {
|
||||
Command::AddKey { exclusive, .. } => {
|
||||
Command::AddKey {
|
||||
exclusive,
|
||||
generate_credential,
|
||||
..
|
||||
} => {
|
||||
let (existing_secret, _) = other_secret("Current password", false)?;
|
||||
let (new_secret, cred) = secret(true)?;
|
||||
let (new_secret, cred) = if *generate_credential && luks2 {
|
||||
let cred = make_credential_id(
|
||||
Some(derive_credential_name(luks.device.as_path()).as_str()),
|
||||
(if authenticator.pin && may_require_pin()? {
|
||||
//TODO: not ideal since it ignores pin-prefixed
|
||||
Some(read_pin()?)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.as_deref(),
|
||||
)?;
|
||||
log(&|| {
|
||||
format!(
|
||||
"generated credential: {}\ncredential username: {:?}",
|
||||
hex::encode(&cred.id),
|
||||
derive_credential_name(luks.device.as_path())
|
||||
)
|
||||
});
|
||||
let creds = vec![HexEncoded(cred.id)];
|
||||
secret("Password to be added", true, &creds)
|
||||
} else {
|
||||
secret("Password to be added", true, &credentials)
|
||||
}?;
|
||||
log(&|| format!("credential used: {}", hex::encode(&cred.id)));
|
||||
let added_slot = luks_dev.add_key(
|
||||
&new_secret,
|
||||
&existing_secret[..],
|
||||
luks_mod.kdf_time.or(Some(10)),
|
||||
Some(&cred.id[..]).filter(|_| *token),
|
||||
Some(&cred.id[..])
|
||||
.filter(|_| !luks.disable_token || *generate_credential)
|
||||
.filter(|_| luks2),
|
||||
)?;
|
||||
if *exclusive {
|
||||
let destroyed = luks_dev.remove_keyslots(&[added_slot])?;
|
||||
@@ -274,24 +379,42 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Command::ReplaceKey { add_password, .. } => {
|
||||
let (existing_secret, _) = secret(false)?;
|
||||
Command::ReplaceKey {
|
||||
add_password,
|
||||
remove_cred,
|
||||
..
|
||||
} => {
|
||||
let (existing_secret, _prev_cred) =
|
||||
secret("Current password", false, &credentials)?;
|
||||
let (replacement_secret, cred) = other_secret("Replacement password", true)?;
|
||||
let slot = if *add_password {
|
||||
luks_dev.add_key(
|
||||
&replacement_secret[..],
|
||||
&existing_secret,
|
||||
luks_mod.kdf_time,
|
||||
cred.as_ref().filter(|_| *token).map(|cred| &cred.id[..]),
|
||||
cred.as_ref()
|
||||
.filter(|_| !luks.disable_token)
|
||||
.filter(|_| luks2)
|
||||
.map(|cred| &cred.id[..]),
|
||||
)
|
||||
} else {
|
||||
luks_dev.replace_key(
|
||||
let slot = luks_dev.replace_key(
|
||||
&replacement_secret[..],
|
||||
&existing_secret,
|
||||
luks_mod.kdf_time,
|
||||
cred.as_ref().filter(|_| *token).map(|cred| &cred.id[..]),
|
||||
)
|
||||
cred.as_ref()
|
||||
.filter(|_| !luks.disable_token)
|
||||
.filter(|_| luks2)
|
||||
.map(|cred| &cred.id[..]),
|
||||
)?;
|
||||
if *remove_cred && cred.is_none() {
|
||||
luks_dev.remove_token_slot(slot)?;
|
||||
}
|
||||
Ok(slot)
|
||||
}?;
|
||||
if let Some(cred) = cred {
|
||||
log(&|| format!("credential used: {}", hex::encode(&cred.id)));
|
||||
}
|
||||
println!(
|
||||
"Added to password to device {}, slot: {}",
|
||||
luks.device.display(),
|
||||
@@ -307,62 +430,71 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
authenticator,
|
||||
secret,
|
||||
name,
|
||||
credentials,
|
||||
retries,
|
||||
..
|
||||
}
|
||||
| Command::OpenToken {
|
||||
luks,
|
||||
authenticator,
|
||||
secret,
|
||||
name,
|
||||
retries,
|
||||
dry_run,
|
||||
} => {
|
||||
let pin_string;
|
||||
let pin = if authenticator.pin {
|
||||
pin_string = read_pin(authenticator)?;
|
||||
Some(pin_string.as_ref())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let salt = |q: &str, verify: bool| -> Fido2LuksResult<[u8; 32]> {
|
||||
if interactive || secret.password_helper == PasswordHelper::Stdin {
|
||||
util::read_password_hashed(q, verify)
|
||||
} else {
|
||||
secret.salt.obtain_sha256(&secret.password_helper)
|
||||
}
|
||||
let inputs = |q: &str, verify: bool| -> Fido2LuksResult<(Option<String>, [u8; 32])> {
|
||||
get_input(&secret, &authenticator, args.interactive, q, verify)
|
||||
};
|
||||
|
||||
// Cow shouldn't be necessary
|
||||
let secret = |credentials: Cow<'_, Vec<HexEncoded>>| {
|
||||
let (pin, salt) = inputs("Password", false)?;
|
||||
prompt_interaction(interactive);
|
||||
derive_secret(
|
||||
credentials.as_ref(),
|
||||
&salt("Password", false)?,
|
||||
&salt,
|
||||
authenticator.await_time,
|
||||
pin,
|
||||
pin.as_deref(),
|
||||
)
|
||||
};
|
||||
|
||||
let mut retries = *retries;
|
||||
let mut luks_dev = LuksDevice::load(&luks.device)?;
|
||||
let luks2 = luks_dev.is_luks2()?;
|
||||
log(&|| format!("luks2 supported: {}", luks2));
|
||||
loop {
|
||||
let secret = match &args.command {
|
||||
Command::Open { credentials, .. } => secret(Cow::Borrowed(&credentials.ids.0))
|
||||
.and_then(|(secret, _cred)| luks_dev.activate(&name, &secret, luks.slot)),
|
||||
Command::OpenToken { .. } => luks_dev.activate_token(
|
||||
let slot = if let Some(ref credentials) = credentials.ids {
|
||||
log(&|| {
|
||||
format!(
|
||||
"credentials: {}",
|
||||
credentials
|
||||
.0
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
});
|
||||
secret(Cow::Borrowed(&credentials.0)).and_then(|(secret, cred)| {
|
||||
log(&|| format!("credential used: {}", hex::encode(&cred.id)));
|
||||
luks_dev.activate(&name, &secret, luks.slot, *dry_run)
|
||||
})
|
||||
} else if luks2 && !luks.disable_token {
|
||||
luks_dev.activate_token(
|
||||
&name,
|
||||
Box::new(|credentials: Vec<String>| {
|
||||
log(&|| format!("credentials: {}", credentials.join(", ")));
|
||||
let creds = credentials
|
||||
.into_iter()
|
||||
.flat_map(|cred| HexEncoded::from_str(cred.as_ref()).ok())
|
||||
.collect::<Vec<_>>();
|
||||
secret(Cow::Owned(creds))
|
||||
.map(|(secret, cred)| (secret, hex::encode(&cred.id)))
|
||||
secret(Cow::Owned(creds)).map(|(secret, cred)| {
|
||||
log(&|| format!("credential used: {}", hex::encode(&cred.id)));
|
||||
(secret, hex::encode(&cred.id))
|
||||
})
|
||||
}),
|
||||
luks.slot,
|
||||
),
|
||||
_ => unreachable!(),
|
||||
*dry_run,
|
||||
)
|
||||
} else if luks_dev.is_luks2()? && luks.disable_token {
|
||||
// disable-token is mostly cosmetic in this instance
|
||||
return Err(Fido2LuksError::InsufficientCredentials);
|
||||
} else {
|
||||
return Err(Fido2LuksError::WrongSecret);
|
||||
};
|
||||
match secret {
|
||||
match slot {
|
||||
Err(e) => {
|
||||
match e {
|
||||
Fido2LuksError::WrongSecret if retries > 0 => {}
|
||||
@@ -370,11 +502,14 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
if cause.kind() == FidoErrorKind::Timeout && retries > 0 => {}
|
||||
|
||||
e => return Err(e),
|
||||
}
|
||||
};
|
||||
retries -= 1;
|
||||
eprintln!("{}", e);
|
||||
}
|
||||
res => break res.map(|_| ()),
|
||||
Ok(slot) => {
|
||||
log(&|| format!("keyslot: {}", slot));
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,7 +579,7 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
}
|
||||
}
|
||||
let count = if tokens.is_empty() {
|
||||
dev.add_token(&Fido2LuksToken::with_credentials(&credentials.ids.0, *slot))?;
|
||||
dev.add_token(&Fido2LuksToken::with_credentials(&credentials.0, *slot))?;
|
||||
1
|
||||
} else {
|
||||
tokens.len()
|
||||
@@ -452,7 +587,7 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
for (id, mut token) in tokens {
|
||||
token
|
||||
.credential
|
||||
.extend(credentials.ids.0.iter().map(|h| h.to_string()));
|
||||
.extend(credentials.0.iter().map(|h| h.to_string()));
|
||||
dev.update_token(id, &token)?;
|
||||
}
|
||||
println!("Updated {} tokens", count);
|
||||
@@ -480,7 +615,7 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
token.credential = token
|
||||
.credential
|
||||
.into_iter()
|
||||
.filter(|cred| !credentials.ids.0.iter().any(|h| &h.to_string() == cred))
|
||||
.filter(|cred| !credentials.0.iter().any(|h| &h.to_string() == cred))
|
||||
.collect();
|
||||
dev.update_token(id, &token)?;
|
||||
}
|
||||
@@ -510,15 +645,25 @@ pub fn run_cli() -> Fido2LuksResult<()> {
|
||||
}
|
||||
},
|
||||
Command::GenerateCompletions { shell, out_dir } => {
|
||||
Args::clap().gen_completions(
|
||||
env!("CARGO_PKG_NAME"),
|
||||
match shell.as_ref() {
|
||||
"bash" => Shell::Bash,
|
||||
"fish" => Shell::Fish,
|
||||
_ => unreachable!("structopt shouldn't allow us to reach this point"),
|
||||
},
|
||||
&out_dir,
|
||||
);
|
||||
// zsh won't work atm https://github.com/clap-rs/clap/issues/1822
|
||||
if let Some(s) = shell {
|
||||
if s.as_str() == "zsh" {
|
||||
unimplemented!("zsh completions are broken atm: see https://github.com/clap-rs/clap/issues/1822")
|
||||
}
|
||||
}
|
||||
for variant in Shell::variants().iter().filter(|v| *v != &"zsh") {
|
||||
if let Some(s) = shell {
|
||||
if *variant != s.as_str() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Args::clap().gen_completions(
|
||||
env!("CARGO_PKG_NAME"),
|
||||
Shell::from_str(variant)
|
||||
.expect("structopt shouldn't allow us to reach this point"),
|
||||
&out_dir,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -531,20 +676,33 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_read_password_pin_prefixed() {
|
||||
// 1234:test -> PIN: 1234, password: test
|
||||
assert_eq!(
|
||||
read_password_pin_prefixed(|| OK("1234:test")),
|
||||
Ok((Some("1234".to_string()), util::sha256(&["test".as_bytes()])))
|
||||
read_password_pin_prefixed(|| Ok("1234:test".into())).unwrap(),
|
||||
(Some("1234".to_string()), util::sha256(&["test".as_bytes()]))
|
||||
);
|
||||
// :test -> PIN: None, password: test
|
||||
assert_eq!(
|
||||
read_password_pin_prefixed(|| OK(":test")),
|
||||
Ok((None, util::sha256(&["test".as_bytes()])))
|
||||
read_password_pin_prefixed(|| Ok(":test".into())).unwrap(),
|
||||
(None, util::sha256(&["test".as_bytes()]))
|
||||
);
|
||||
// 1234::test -> PIN: 1234, password: :test
|
||||
assert_eq!(
|
||||
read_password_pin_prefixed(|| OK("1234::test")),
|
||||
Ok((
|
||||
read_password_pin_prefixed(|| Ok("1234::test".into())).unwrap(),
|
||||
(
|
||||
Some("1234".to_string()),
|
||||
util::sha256(&[":test".as_bytes()])
|
||||
))
|
||||
)
|
||||
);
|
||||
// 1234 -> PIN: 1234, password: empty
|
||||
assert_eq!(
|
||||
read_password_pin_prefixed(|| Ok("1234".into())).unwrap(),
|
||||
(Some("1234".to_string()), util::sha256(&["".as_bytes()]))
|
||||
);
|
||||
// 1234:test -> PIN: None, password: test
|
||||
assert_eq!(
|
||||
read_password_pin_prefixed(|| Ok(":test".into())).unwrap(),
|
||||
(None, util::sha256(&["test".as_bytes()]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -55,11 +56,17 @@ impl fmt::Display for SecretInput {
|
||||
}
|
||||
|
||||
impl SecretInput {
|
||||
pub fn obtain_string(&self, password_helper: &PasswordHelper) -> Fido2LuksResult<String> {
|
||||
pub fn obtain_string(
|
||||
&self,
|
||||
password_helper: Option<impl FnOnce() -> Fido2LuksResult<String>>,
|
||||
) -> Fido2LuksResult<String> {
|
||||
Ok(String::from_utf8(self.obtain(password_helper)?)?)
|
||||
}
|
||||
|
||||
pub fn obtain(&self, password_helper: &PasswordHelper) -> Fido2LuksResult<Vec<u8>> {
|
||||
pub fn obtain(
|
||||
&self,
|
||||
password_helper: Option<impl FnOnce() -> Fido2LuksResult<String>>,
|
||||
) -> Fido2LuksResult<Vec<u8>> {
|
||||
let mut secret = Vec::new();
|
||||
match self {
|
||||
SecretInput::File { path } => {
|
||||
@@ -67,16 +74,22 @@ impl SecretInput {
|
||||
let mut do_io = || File::open(path)?.read_to_end(&mut secret);
|
||||
do_io().map_err(|cause| Fido2LuksError::KeyfileError { cause })?;
|
||||
}
|
||||
SecretInput::AskPassword => {
|
||||
secret.extend_from_slice(password_helper.obtain()?.as_bytes())
|
||||
}
|
||||
SecretInput::AskPassword => secret.extend_from_slice(
|
||||
password_helper.ok_or_else(|| Fido2LuksError::AskPassError {
|
||||
cause: AskPassError::FailedHelper,
|
||||
})?()?
|
||||
.as_bytes(),
|
||||
),
|
||||
|
||||
SecretInput::String(s) => secret.extend_from_slice(s.as_bytes()),
|
||||
}
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
pub fn obtain_sha256(&self, password_helper: &PasswordHelper) -> Fido2LuksResult<[u8; 32]> {
|
||||
pub fn obtain_sha256(
|
||||
&self,
|
||||
password_helper: Option<impl FnOnce() -> Fido2LuksResult<String>>,
|
||||
) -> Fido2LuksResult<[u8; 32]> {
|
||||
let mut digest = digest::Context::new(&digest::SHA256);
|
||||
match self {
|
||||
SecretInput::File { path } => {
|
||||
@@ -151,11 +164,13 @@ impl PasswordHelper {
|
||||
use PasswordHelper::*;
|
||||
match self {
|
||||
Systemd => unimplemented!(),
|
||||
Stdin => Ok(util::read_password("Password", true)?),
|
||||
Stdin => Ok(util::read_password("Password", true, false)?),
|
||||
Script(password_helper) => {
|
||||
let password = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&password_helper)
|
||||
.stdin(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.map_err(|e| Fido2LuksError::AskPassError {
|
||||
cause: error::AskPassError::IO(e),
|
||||
@@ -198,7 +213,7 @@ mod test {
|
||||
fn input_salt_obtain() {
|
||||
assert_eq!(
|
||||
SecretInput::String("abc".into())
|
||||
.obtain(&PasswordHelper::Stdin)
|
||||
.obtain_sha256(Some(|| Ok("123456".to_string())))
|
||||
.unwrap(),
|
||||
[
|
||||
186, 120, 22, 191, 143, 1, 207, 234, 65, 65, 64, 222, 93, 174, 34, 35, 176, 3, 97,
|
||||
|
@@ -1,7 +1,8 @@
|
||||
use std::fmt::{Display, Error, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use structopt::clap::AppSettings;
|
||||
use structopt::clap::{AppSettings, Shell};
|
||||
use structopt::StructOpt;
|
||||
|
||||
mod config;
|
||||
@@ -31,6 +32,12 @@ impl FromStr for HexEncoded {
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for HexEncoded {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct CommaSeparated<T: FromStr + Display>(pub Vec<T>);
|
||||
|
||||
@@ -69,7 +76,7 @@ pub struct Credentials {
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct AuthenticatorParameters {
|
||||
/// Request a PIN to unlock the authenticator
|
||||
/// Request a PIN to unlock the authenticator if required
|
||||
#[structopt(short = "P", long = "pin")]
|
||||
pub pin: bool,
|
||||
|
||||
@@ -97,7 +104,10 @@ pub struct LuksParameters {
|
||||
pub slot: Option<u32>,
|
||||
|
||||
/// Disable implicit use of LUKS2 tokens
|
||||
#[structopt(long = "disable-token", env = "FIDO2LUKS_DISABLE_TOKEN")]
|
||||
#[structopt(
|
||||
long = "disable-token",
|
||||
// env = "FIDO2LUKS_DISABLE_TOKEN" // unfortunately clap will convert flags into args if they have an env attribute
|
||||
)]
|
||||
pub disable_token: bool,
|
||||
}
|
||||
|
||||
@@ -128,8 +138,7 @@ pub struct SecretParameters {
|
||||
#[structopt(
|
||||
name = "password-helper",
|
||||
env = "FIDO2LUKS_PASSWORD_HELPER",
|
||||
long = "password-helper",
|
||||
default_value = "/usr/bin/env systemd-ask-password 'Please enter second factor for LUKS disk encryption!'"
|
||||
long = "password-helper"
|
||||
)]
|
||||
pub password_helper: Option<PasswordHelper>,
|
||||
}
|
||||
@@ -138,6 +147,8 @@ pub struct Args {
|
||||
/// Request passwords via Stdin instead of using the password helper
|
||||
#[structopt(short = "i", long = "interactive")]
|
||||
pub interactive: bool,
|
||||
#[structopt(short = "v", long = "verbose")]
|
||||
pub verbose: bool,
|
||||
#[structopt(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
@@ -185,9 +196,9 @@ pub enum Command {
|
||||
/// Will wipe all other keys
|
||||
#[structopt(short = "e", long = "exclusive")]
|
||||
exclusive: bool,
|
||||
/// Will generate an credential for only this device
|
||||
#[structopt(short = "a", long = "auto-cred")]
|
||||
auto_credential: bool,
|
||||
/// Will generate an credential while adding a new key to this LUKS device if supported
|
||||
#[structopt(short = "g", long = "gen-cred")]
|
||||
generate_credential: bool,
|
||||
#[structopt(flatten)]
|
||||
existing_secret: OtherSecret,
|
||||
#[structopt(flatten)]
|
||||
@@ -207,7 +218,7 @@ pub enum Command {
|
||||
/// Add the password and keep the key
|
||||
#[structopt(short = "a", long = "add-password")]
|
||||
add_password: bool,
|
||||
/// Remove the affected credential for device header
|
||||
/// Remove the affected credential from LUKS header
|
||||
#[structopt(short = "r", long = "remove-cred")]
|
||||
remove_cred: bool,
|
||||
#[structopt(flatten)]
|
||||
@@ -216,7 +227,7 @@ pub enum Command {
|
||||
luks_mod: LuksModParameters,
|
||||
},
|
||||
/// Open the LUKS device
|
||||
#[structopt(name = "open")]
|
||||
#[structopt(name = "open", alias = "open-token")]
|
||||
Open {
|
||||
#[structopt(flatten)]
|
||||
luks: LuksParameters,
|
||||
@@ -230,26 +241,30 @@ pub enum Command {
|
||||
secret: SecretParameters,
|
||||
#[structopt(short = "r", long = "max-retries", default_value = "0")]
|
||||
retries: i32,
|
||||
/// Perform the whole procedure without mounting the LUKS volume on success
|
||||
#[structopt(long = "dry-run")]
|
||||
dry_run: bool,
|
||||
},
|
||||
/// Generate a new FIDO credential
|
||||
#[structopt(name = "credential")]
|
||||
Credential {
|
||||
#[structopt(flatten)]
|
||||
authenticator: AuthenticatorParameters,
|
||||
/// Name to be displayed on the authenticator if it has a display
|
||||
#[structopt(env = "FIDO2LUKS_CREDENTIAL_NAME")]
|
||||
name: Option<String>,
|
||||
/// Name to be displayed on the authenticator display
|
||||
#[structopt(env = "FIDO2LUKS_CREDENTIAL_NAME", default_value = "fido2luks")]
|
||||
name: String,
|
||||
},
|
||||
/// Check if an authenticator is connected
|
||||
#[structopt(name = "connected")]
|
||||
Connected,
|
||||
Token(TokenCommand),
|
||||
/// Generate bash completion scripts
|
||||
/// Example: fido2luks completions --shell bash /usr/share/bash-completion/completions
|
||||
#[structopt(name = "completions", setting = AppSettings::Hidden)]
|
||||
GenerateCompletions {
|
||||
/// Shell to generate completions for: bash, fish
|
||||
#[structopt(possible_values = &["bash", "fish"])]
|
||||
shell: String,
|
||||
/// Shell to generate completions for
|
||||
#[structopt(short = "s", long = "shell",possible_values = &Shell::variants()[..])]
|
||||
shell: Option<String>,
|
||||
out_dir: PathBuf,
|
||||
},
|
||||
}
|
||||
@@ -269,8 +284,14 @@ pub enum TokenCommand {
|
||||
Add {
|
||||
#[structopt(env = "FIDO2LUKS_DEVICE")]
|
||||
device: PathBuf,
|
||||
#[structopt(flatten)]
|
||||
credentials: Credentials,
|
||||
/// FIDO credential ids, separated by ',' generate using fido2luks credential
|
||||
#[structopt(
|
||||
name = "credential-ids",
|
||||
env = "FIDO2LUKS_CREDENTIAL_ID",
|
||||
short = "c",
|
||||
long = "creds"
|
||||
)]
|
||||
credentials: CommaSeparated<HexEncoded>,
|
||||
/// Slot to which the credentials will be added
|
||||
#[structopt(long = "slot", env = "FIDO2LUKS_DEVICE_SLOT")]
|
||||
slot: u32,
|
||||
@@ -279,8 +300,14 @@ pub enum TokenCommand {
|
||||
Remove {
|
||||
#[structopt(env = "FIDO2LUKS_DEVICE")]
|
||||
device: PathBuf,
|
||||
#[structopt(flatten)]
|
||||
credentials: Credentials,
|
||||
/// FIDO credential ids, separated by ',' generate using fido2luks credential
|
||||
#[structopt(
|
||||
name = "credential-ids",
|
||||
env = "FIDO2LUKS_CREDENTIAL_ID",
|
||||
short = "c",
|
||||
long = "creds"
|
||||
)]
|
||||
credentials: CommaSeparated<HexEncoded>,
|
||||
/// Token from which the credentials will be removed
|
||||
#[structopt(long = "token")]
|
||||
token_id: Option<u32>,
|
||||
|
@@ -19,7 +19,7 @@ pub fn make_credential_id(
|
||||
}
|
||||
let request = request.build().unwrap();
|
||||
let make_credential = |device: &mut FidoDevice| {
|
||||
if let Some(pin) = pin {
|
||||
if let Some(pin) = pin.filter(|_| device.needs_pin()) {
|
||||
device.unlock(pin)?;
|
||||
}
|
||||
device.make_hmac_credential(&request)
|
||||
@@ -44,7 +44,7 @@ pub fn perform_challenge<'a>(
|
||||
.build()
|
||||
.unwrap();
|
||||
let get_assertion = |device: &mut FidoDevice| {
|
||||
if let Some(pin) = pin {
|
||||
if let Some(pin) = pin.filter(|_| device.needs_pin()) {
|
||||
device.unlock(pin)?;
|
||||
}
|
||||
device.get_hmac_assertion(&request, &util::sha256(&[&salt[..]]), None)
|
||||
@@ -58,6 +58,17 @@ pub fn perform_challenge<'a>(
|
||||
Ok((secret, credential))
|
||||
}
|
||||
|
||||
pub fn may_require_pin() -> Fido2LuksResult<bool> {
|
||||
for di in ctap::get_devices()? {
|
||||
if let Ok(dev) = FidoDevice::new(&di) {
|
||||
if dev.needs_pin() {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub fn get_devices() -> Fido2LuksResult<Vec<FidoDevice>> {
|
||||
let mut devices = Vec::with_capacity(2);
|
||||
for di in ctap::get_devices()? {
|
||||
|
@@ -29,6 +29,10 @@ pub enum Fido2LuksError {
|
||||
WrongSecret,
|
||||
#[fail(display = "not an utf8 string")]
|
||||
StringEncodingError { cause: FromUtf8Error },
|
||||
#[fail(display = "not an hex string: {}", string)]
|
||||
HexEncodingError { string: String },
|
||||
#[fail(display = "couldn't obtain at least one credential")]
|
||||
InsufficientCredentials,
|
||||
}
|
||||
|
||||
impl Fido2LuksError {
|
||||
@@ -50,6 +54,8 @@ pub enum AskPassError {
|
||||
IO(io::Error),
|
||||
#[fail(display = "provided passwords don't match")]
|
||||
Mismatch,
|
||||
#[fail(display = "failed to call password helper")]
|
||||
FailedHelper,
|
||||
}
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
|
29
src/luks.rs
29
src/luks.rs
@@ -111,6 +111,20 @@ impl LuksDevice {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_token_slot(&mut self, slot: u32) -> Fido2LuksResult<()> {
|
||||
let mut remove = HashSet::new();
|
||||
for token in self.tokens()? {
|
||||
let (id, token) = token?;
|
||||
if token.keyslots.contains(&slot.to_string()) {
|
||||
remove.insert(id);
|
||||
}
|
||||
}
|
||||
for rm in remove {
|
||||
self.remove_token(rm)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_token(&mut self, token: u32, data: &Fido2LuksToken) -> Fido2LuksResult<()> {
|
||||
self.require_luks2()?;
|
||||
self.device
|
||||
@@ -192,7 +206,9 @@ impl LuksDevice {
|
||||
old_secret,
|
||||
CryptActivateFlags::empty(),
|
||||
)?;
|
||||
self.device.keyslot_handle().change_by_passphrase(
|
||||
|
||||
// slot should stay the same but better be safe than sorry
|
||||
let slot = self.device.keyslot_handle().change_by_passphrase(
|
||||
Some(slot),
|
||||
Some(slot),
|
||||
old_secret,
|
||||
@@ -221,10 +237,16 @@ impl LuksDevice {
|
||||
name: &str,
|
||||
secret: &[u8],
|
||||
slot_hint: Option<u32>,
|
||||
dry_run: bool,
|
||||
) -> Fido2LuksResult<u32> {
|
||||
self.device
|
||||
.activate_handle()
|
||||
.activate_by_passphrase(Some(name), slot_hint, secret, CryptActivateFlags::empty())
|
||||
.activate_by_passphrase(
|
||||
Some(name).filter(|_| !dry_run),
|
||||
slot_hint,
|
||||
secret,
|
||||
CryptActivateFlags::empty(),
|
||||
)
|
||||
.map_err(LuksError::activate)
|
||||
}
|
||||
|
||||
@@ -233,6 +255,7 @@ impl LuksDevice {
|
||||
name: &str,
|
||||
secret: impl Fn(Vec<String>) -> Fido2LuksResult<([u8; 32], String)>,
|
||||
slot_hint: Option<u32>,
|
||||
dry_run: bool,
|
||||
) -> Fido2LuksResult<u32> {
|
||||
if !self.is_luks2()? {
|
||||
return Err(LuksError::Luks2Required.into());
|
||||
@@ -276,7 +299,7 @@ impl LuksDevice {
|
||||
.chain(std::iter::once(None).take(slots.is_empty() as usize)), // Try all slots as last resort
|
||||
);
|
||||
for slot in slots {
|
||||
match self.activate(name, &secret, slot) {
|
||||
match self.activate(name, &secret, slot, dry_run) {
|
||||
Err(Fido2LuksError::WrongSecret) => (),
|
||||
res => return res,
|
||||
}
|
||||
|
18
src/util.rs
18
src/util.rs
@@ -13,9 +13,17 @@ pub fn sha256(messages: &[&[u8]]) -> [u8; 32] {
|
||||
secret.as_mut().copy_from_slice(digest.finish().as_ref());
|
||||
secret
|
||||
}
|
||||
|
||||
pub fn read_password(q: &str, verify: bool) -> Fido2LuksResult<String> {
|
||||
match rpassword::read_password_from_tty(Some(&[q, ": "].join("")))? {
|
||||
pub fn read_password_tty(q: &str, verify: bool) -> Fido2LuksResult<String> {
|
||||
read_password(q, verify, true)
|
||||
}
|
||||
pub fn read_password(q: &str, verify: bool, tty: bool) -> Fido2LuksResult<String> {
|
||||
let res = if tty {
|
||||
rpassword::read_password_from_tty(Some(&[q, ": "].join("")))
|
||||
} else {
|
||||
print!("{}: ", q);
|
||||
rpassword::read_password()
|
||||
}?;
|
||||
match res {
|
||||
ref pass
|
||||
if verify
|
||||
&& &rpassword::read_password_from_tty(Some(&[q, "(again): "].join(" ")))?
|
||||
@@ -29,10 +37,6 @@ pub fn read_password(q: &str, verify: bool) -> Fido2LuksResult<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_password_hashed(q: &str, verify: bool) -> Fido2LuksResult<[u8; 32]> {
|
||||
read_password(q, verify).map(|pass| sha256(&[pass.as_bytes()]))
|
||||
}
|
||||
|
||||
pub fn read_keyfile<P: Into<PathBuf>>(path: P) -> Fido2LuksResult<Vec<u8>> {
|
||||
let mut file = File::open(path.into())?;
|
||||
let mut key = Vec::new();
|
||||
|
Reference in New Issue
Block a user