diff --git a/.drone.yml b/.drone.yml index 7e3a505..00daafa 100644 --- a/.drone.yml +++ b/.drone.yml @@ -12,7 +12,7 @@ steps: environment: DEBIAN_FRONTEND: noninteractive commands: - - apt update && apt install -y cargo libkeyutils-dev libclang-dev clang pkg-config libcryptsetup-dev + - apt update && apt install -y cargo libkeyutils-dev libclang-dev clang pkg-config libcryptsetup-dev libpam-dev - cargo test --locked - name: publish image: ubuntu:focal @@ -22,7 +22,7 @@ steps: from_secret: cargo_tkn commands: - grep -E 'version ?= ?"${DRONE_TAG}"' -i Cargo.toml || (printf "incorrect crate/tag version" && exit 1) - - apt update && apt install -y cargo libkeyutils-dev libclang-dev clang pkg-config libcryptsetup-dev + - apt update && apt install -y cargo libkeyutils-dev libclang-dev clang pkg-config libcryptsetup-dev libpam-dev - cargo package --all-features - cargo publish --all-features when: diff --git a/Cargo.lock b/Cargo.lock index b91b404..04e4dd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,18 +377,20 @@ dependencies = [ [[package]] name = "fido2luks" -version = "0.2.14" +version = "0.2.15" dependencies = [ "ctap_hmac", "failure", "hex", "libcryptsetup-rs", + "pamsm", "ring", "rpassword", "serde", "serde_derive", "serde_json", "structopt", + "sudo", ] [[package]] @@ -596,6 +598,12 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" +[[package]] +name = "pamsm" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3580ed2ebe075c74db583233318abf4b07bc8d9a40c7691d0ae9c186e19e43dd" + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -980,6 +988,16 @@ dependencies = [ "syn 1.0.40", ] +[[package]] +name = "sudo" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a88e74edf206f281aff2820aa2066c781331044c770626dcafe19491f214e05" +dependencies = [ + "libc", + "log", +] + [[package]] name = "syn" version = "0.15.44" diff --git a/Cargo.toml b/Cargo.toml index ad486f0..3674cf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fido2luks" -version = "0.2.14" +version = "0.2.15" authors = ["shimunn "] edition = "2018" @@ -24,6 +24,8 @@ libcryptsetup-rs = "0.4.1" serde_json = "1.0.51" serde_derive = "1.0.106" serde = "1.0.106" +pamsm = { version = "0.4.1", features = ["libpam"] } +sudo = "0.5.0" [build-dependencies] ctap_hmac = { version="0.4.2", features = ["request_multiple"] } @@ -32,6 +34,7 @@ ring = "0.13.5" failure = "0.1.5" rpassword = "4.0.1" libcryptsetup-rs = "0.4.1" +pamsm = { version = "0.4.1", features = ["libpam"] } structopt = "0.3.2" [profile.release] @@ -41,12 +44,22 @@ panic = 'abort' incremental = false overflow-checks = false +[[bin]] +name = "fido2luks" +path = "src/main.rs" + +[lib] +name = "fido2luks_pam" +path = "src/lib.rs" +crate-type = ["cdylib"] + [package.metadata.deb] depends = "$auto, cryptsetup" -build-depends = "libclang-dev, libcryptsetup-dev" +build-depends = "libclang-dev, libcryptsetup-dev, libpam-dev" extended-description = "Decrypt your LUKS partition using a FIDO2 compatible authenticator" assets = [ ["target/release/fido2luks", "usr/bin/", "755"], + ["target/release/libfido2luks_pam.so", "usr/lib/x86_64-linux-gnu/security/pam_fido2luks.so", "755"], ["fido2luks.bash", "usr/share/bash-completion/completions/fido2luks", "644"], ["initramfs-tools/keyscript.sh", "/lib/cryptsetup/scripts/fido2luks", "755" ], ["initramfs-tools/hook/fido2luks.sh", "etc/initramfs-tools/hooks/", "755" ], diff --git a/src/cli_args/config.rs b/src/cli_args/config.rs index d2ef46c..cde093f 100644 --- a/src/cli_args/config.rs +++ b/src/cli_args/config.rs @@ -198,7 +198,7 @@ mod test { fn input_salt_obtain() { assert_eq!( SecretInput::String("abc".into()) - .obtain(&PasswordHelper::Stdin) + .obtain_sha256(&PasswordHelper::Stdin) .unwrap(), [ 186, 120, 22, 191, 143, 1, 207, 234, 65, 65, 64, 222, 93, 174, 34, 35, 176, 3, 97, diff --git a/src/error.rs b/src/error.rs index 03d9741..bcaa5cd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,10 +1,10 @@ use ctap::FidoError; use libcryptsetup_rs::LibcryptErr; +use pamsm::PamError; use std::io; use std::io::ErrorKind; use std::string::FromUtf8Error; use Fido2LuksError::*; - pub type Fido2LuksResult = Result; #[derive(Debug, Fail)] @@ -29,6 +29,10 @@ pub enum Fido2LuksError { WrongSecret, #[fail(display = "not an utf8 string")] StringEncodingError { cause: FromUtf8Error }, + #[fail(display = "elevated privileges required")] + MissingPrivileges, + #[fail(display = "{}", cause)] + Configuration { cause: ConfigurationError }, } impl Fido2LuksError { @@ -50,6 +54,26 @@ pub enum AskPassError { IO(io::Error), #[fail(display = "provided passwords don't match")] Mismatch, + #[fail(display = "unable to retrieve password: {}", _0)] + Pam(PamError), +} + +impl From for AskPassError { + fn from(e: PamError) -> Self { + AskPassError::Pam(e) + } +} + +impl From for AskPassError { + fn from(e: io::Error) -> Self { + AskPassError::IO(e) + } +} + +impl From for Fido2LuksError { + fn from(cause: AskPassError) -> Self { + Fido2LuksError::AskPassError { cause } + } } #[derive(Debug, Fail)] @@ -112,3 +136,16 @@ impl From for Fido2LuksError { StringEncodingError { cause: e } } } +#[derive(Debug, Fail)] +pub enum ConfigurationError { + #[fail(display = "config is missing some values: {:?}", _0)] + Missing(Vec), + #[fail(display = "config attribute {} contains an invalid value: {}", _1, _0)] + InvalidValue(String, String), +} + +impl From for Fido2LuksError { + fn from(cause: ConfigurationError) -> Fido2LuksError { + Fido2LuksError::Configuration { cause } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d476438 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,183 @@ +#[macro_use] +extern crate failure; +extern crate ctap_hmac as ctap; +#[macro_use] +extern crate serde_derive; +use crate::cli_args::{CommaSeparated, HexEncoded}; +use crate::device::*; +use crate::error::*; +use crate::luks::*; +use ctap::FidoCredential; +use failure::_core::time::Duration; +use pamsm::PamLibExt; +use pamsm::*; +use std::collections::{HashMap, HashSet}; +use std::path::Path; +use std::str::FromStr; +use sudo::{self, RunningAs}; + +pub mod cli_args; +pub mod device; +pub mod error; +pub mod luks; +pub mod util; + +struct PamFido2Luks; + +impl PamFido2Luks { + fn open( + &self, + user: String, + mut password: impl FnMut(&str) -> PamResult, + args: Vec, + ) -> Fido2LuksResult<()> { + let args: HashMap = args + .into_iter() + .filter_map(|arg| { + let mut parts = arg.split("="); + parts + .by_ref() + .next() + .map(|key| (key.to_string(), parts.collect::>().join("="))) + }) + .collect(); + + let credentials = match args.get("credentials").map(|creds| { + >::from_str(creds) + .map(|cs| cs.0) + .map_err(|_| ConfigurationError::InvalidValue("credentials".into(), creds.into())) + }) { + Some(creds) => creds?, + _ => Vec::new(), + }; + let pin = args.get("pin"); + let pin_prefix = args + .get("pin-prefix") + .map(|p| p.parse::().unwrap_or_default()) + .unwrap_or_default(); + let device = args + .get("device") + .map(|device| device.replace("%user%", user.as_str())); + let name = args + .get("name") + .map(|name| name.replace("%user%", user.as_str())); + + let mut attempts = args + .get("attempts") + .and_then(|a| a.parse::().ok()) + .unwrap_or(3); + + if let (Some(device), Some(name)) = (device, name) { + if !Path::new(&device).exists() || Path::new(&format!("/dev/mapper/{}", name)).exists() + { + return Ok(()); + } + // root required to mount luks + match sudo::check() { + RunningAs::User => return Err(Fido2LuksError::MissingPrivileges), + _ => { + sudo::escalate_if_needed().map_err(|_| Fido2LuksError::MissingPrivileges)?; + } + } + let mut device = LuksDevice::load(device)?; + let mut additional_credentials: HashSet = HashSet::new(); + if device.is_luks2()? { + for token in device.tokens()? { + let (_, token) = token?; + additional_credentials.extend(token.credential.into_iter()); + } + } + let credentials: Vec = credentials + .into_iter() + .chain(additional_credentials.into_iter()) + .map(|cred| HexEncoded::from_str(cred.as_str())) + .map(|cred| FidoCredential { + id: cred.unwrap().0, + public_key: None, + }) + .collect(); + let credentials: Vec<&FidoCredential> = credentials.iter().collect(); + if !credentials.is_empty() { + loop { + let (pin, pass) = if pin_prefix { + let password = password("PIN + FIDO2 salt (pin:password):") + .map_err(|e| Fido2LuksError::AskPassError { cause: e.into() })?; + let mut parts = password.split(":"); + ( + parts.next().map(|p| p.to_string()).or(pin.cloned()), + parts.collect::>().join(":"), + ) + } else { + ( + pin.cloned(), + password("FIDO2 salt: ") + .map_err(|e| Fido2LuksError::AskPassError { cause: e.into() })?, + ) + }; + + let salt = util::sha256(&[pass.as_bytes()]); + let secret = util::sha256(&[ + &salt, + &perform_challenge( + &credentials[..], + &salt, + Duration::from_secs(15), + pin.as_ref().map(String::as_str), + )? + .0[..], + ]); + match device.activate(name.as_str(), &secret[..], None) { + Ok(_) => return Ok(()), + _ if attempts > 0 => { + attempts -= 1; + continue; + } + Err(e) => break Err(e), + } + } + } else { + Err(ConfigurationError::Missing(vec!["credentials".into()]).into()) + } + } else { + Ok(()) + } + } +} + +impl PamServiceModule for PamFido2Luks { + fn authenticate(pamh: Pam, _flag: PamFlag, args: Vec) -> PamError { + let perfrom_authenticate = move || -> Fido2LuksResult<()> { + let user = match pamh.get_cached_user() { + Err(e) => Err(AskPassError::Pam(e))?, + Ok(p) => p.map(|s| s.to_str().map(str::to_string).unwrap()), + }; + let mut password = match pamh.get_authtok(None) { + Err(e) => Err(AskPassError::Pam(e))?, + Ok(p) => p.map(|s| s.to_str().map(str::to_string).unwrap()), + }; + if let Some(user) = user { + PamFido2Luks.open( + user, + move |q: &str| match password.take() { + Some(pass) => Ok(pass), + None => pamh + .conv(Some(q), PamMsgStyle::PROMPT_ECHO_OFF) + .map(|s| s.map(|s| s.to_str().unwrap()).unwrap_or("").to_string()), + }, + args, + ) + } else { + Err(AskPassError::Pam(PamError::AUTH_ERR))? + } + }; + match perfrom_authenticate() { + Ok(_) => PamError::SUCCESS, + Err(e) => { + eprintln!("{}", e); + PamError::AUTH_ERR + } + } + } +} + +pam_module!(PamFido2Luks);