Compare commits

..

21 Commits

Author SHA1 Message Date
acd4021e03
Merge remote-tracking branch 'gt/ctap-hid-fido2' into 0.3.0-alpha 2024-03-03 19:41:19 +01:00
238d877e2f
chore: update naersk 2024-02-11 19:37:12 +01:00
93e8a33c0e
chore: update deps 2023-12-17 13:53:14 +01:00
shimunn
fb60987468
build nix package 2023-10-02 11:55:56 +02:00
871b2863b2
fix: build env 2022-09-28 19:48:30 +02:00
f436ae538d
chore: update 2022-07-28 15:10:28 +02:00
17c96090bd
fix: ctap crate 2022-07-05 15:35:54 +02:00
fce6ea2e31
fix: use patched ctap-hid crate 2022-06-16 17:42:50 +02:00
b566af46f7
fix: prevent creation of rk credential 2022-06-15 01:16:06 +02:00
1f0d555cea
added: comment field to luks header data 2022-04-15 17:06:11 +02:00
2255f224a5
bump version 2022-04-11 14:36:07 +02:00
581e1780d1
update ctap-hid 2022-04-10 17:23:25 +02:00
7daa5a3fdb
use develop version 2022-04-04 10:57:57 +02:00
4e986b8f05
removed: keepalive msg 2022-03-29 15:58:51 +02:00
ca82293976
fix: reintroduce connected command 2022-03-29 15:58:16 +02:00
d5b043840f
chore: migrate to ctap-hid-fido2 2022-03-27 10:00:12 +02:00
eb8d65eb4f
switch to ctap-hid-fido2 2022-03-23 19:30:24 +01:00
f6c2bc4cdb
added --allow-discards flag 2021-12-28 13:50:21 +01:00
4e7ef4b8b7
0.3.0-alpha notice 2021-12-11 11:52:40 +01:00
e1ad8b37c1
0.3.0-alpha 2021-12-11 11:51:07 +01:00
e9510216ef
1
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2020-10-11 18:33:59 +02:00
22 changed files with 1427 additions and 1131 deletions

View File

@ -8,22 +8,20 @@ steps:
- rustup component add rustfmt
- cargo fmt --all -- --check
- name: test
image: ubuntu:focal
image: shimun/fido2luks@sha256:6d0b4017bffbec5fac8f25d383d68671fcc9930efb02e97ce5ea81acf0060ece
environment:
DEBIAN_FRONTEND: noninteractive
commands:
- apt update && apt install -y cargo libkeyutils-dev libclang-dev clang pkg-config libcryptsetup-dev
- cargo test --locked
- name: publish
image: ubuntu:focal
image: shimun/fido2luks@sha256:6d0b4017bffbec5fac8f25d383d68671fcc9930efb02e97ce5ea81acf0060ece
environment:
DEBIAN_FRONTEND: noninteractive
CARGO_REGISTRY_TOKEN:
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
- cargo package --all-features
- cargo publish --all-features
- cargo package --all-features --allow-dirty
- cargo publish --all-features --allow-dirty
when:
event: tag

32
.github/workflows/current.yml vendored Normal file
View File

@ -0,0 +1,32 @@
# This is a basic workflow to help you get started with Actions
name: Current
# Controls when the workflow will run
on:
schedule:
- cron: '0 22 * * 6'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v4
- name: Setup Attic cache
uses: ryanccn/attic-action@v0
with:
endpoint: ${{ secrets.ATTIC_ENDPOINT }}
cache: ${{ secrets.ATTIC_CACHE }}
token: ${{ secrets.ATTIC_TOKEN }}
- name: Build Nix Package nixos-unstable
run: nix build --override-input nixpkgs github:nixos/nixpkgs/nixos-unstable --show-trace

33
.github/workflows/locked.yml vendored Normal file
View File

@ -0,0 +1,33 @@
# This is a basic workflow to help you get started with Actions
name: Locked
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the "master" branch
push:
branches: '*'
pull_request:
branches: '*'
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v4
- name: Setup Attic cache
uses: ryanccn/attic-action@v0
with:
endpoint: ${{ secrets.ATTIC_ENDPOINT }}
cache: ${{ secrets.ATTIC_CACHE }}
token: ${{ secrets.ATTIC_TOKEN }}
- name: Build Nix Package
run: nix build -j 10 --show-trace

11
CHANGELOG.md Normal file
View File

@ -0,0 +1,11 @@
## 0.3.0
* LUKS2 Tokens are now supported by every subcommand
* `<credential>` has been converted into the flag `--creds`
credentials provided by `--creds` will be supplemented from the LUKS header unless this is disabled by `--disable-token`
* `fido2luks add-key` will take an `--auto-cred` flag which allows for credentials to be generated and stored without having to use `fido2luks credential`
`fido2luks replace-key` will allow for credentials to be removed using the `--remove-cred` flag respectively
* Removed `fido2luks open-token` subcommand
`fido2luks open` now fulfills both functions
* Added `fido2luks open --dry-run` flag, to perform the whole procedure apart from mounting the LUKS volume
* Added an `--verbose` flag to display additional information like credentials and keyslots used if desired

1217
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "fido2luks"
version = "0.2.20"
version = "0.3.1-alpha"
authors = ["shimunn <shimun@shimun.net>"]
edition = "2018"
@ -14,25 +14,26 @@ categories = ["command-line-utilities"]
license = "MPL-2.0"
[dependencies]
ctap_hmac = { version="0.4.5", features = ["request_multiple"] }
hex = "0.3.2"
ring = "0.13.5"
ring = "0.16.5"
failure = "0.1.5"
rpassword = "4.0.1"
structopt = "0.3.2"
libcryptsetup-rs = "0.4.1"
libcryptsetup-rs = "0.9.1"
serde_json = "1.0.51"
serde_derive = "1.0.106"
serde = "1.0.106"
serde_derive = "1.0.116"
serde = "1.0.116"
anyhow = "1.0.56"
ctap-hid-fido2 = "3.4.1"
[build-dependencies]
ctap_hmac = { version="0.4.5", features = ["request_multiple"] }
hex = "0.3.2"
ring = "0.13.5"
ring = "0.16.5"
failure = "0.1.5"
rpassword = "4.0.1"
libcryptsetup-rs = "0.4.1"
libcryptsetup-rs = "0.9.1"
structopt = "0.3.2"
anyhow = "1.0.56"
[profile.release]
lto = true
@ -48,7 +49,6 @@ extended-description = "Decrypt your LUKS partition using a FIDO2 compatible aut
assets = [
["target/release/fido2luks", "usr/bin/", "755"],
["fido2luks.bash", "usr/share/bash-completion/completions/fido2luks", "644"],
["pam_mount/fido2luksmounthelper.sh", "usr/bin/", "755"],
["initramfs-tools/keyscript.sh", "/lib/cryptsetup/scripts/fido2luks", "755" ],
["initramfs-tools/hook/fido2luks.sh", "etc/initramfs-tools/hooks/", "755" ],
["initramfs-tools/fido2luks.conf", "etc/", "644"],

161
README.md
View File

@ -1,160 +1,7 @@
# fido2luks [![Crates.io Version](https://img.shields.io/crates/v/fido2luks.svg)](https://crates.io/crates/fido2luks)
This will allow you to unlock your LUKS encrypted disk with an FIDO2 compatible key.
Note: This has only been tested under Fedora 31, [Ubuntu 20.04](initramfs-tools/), [NixOS](https://nixos.org/nixos/manual/#sec-luks-file-systems-fido2) using a Solo Key, Trezor Model T, YubiKey(fw >= [5.2.3](https://support.yubico.com/hc/en-us/articles/360016649319-YubiKey-5-2-3-Enhancements-to-FIDO-2-Support))
## Setup
### Prerequisites
```
dnf install clang cargo cryptsetup-devel -y
```
### Device
```
git clone https://github.com/shimunn/fido2luks.git && cd fido2luks
# Alternativly cargo build --release && sudo cp target/release/fido2luks /usr/bin/
sudo -E cargo install -f --path . --root /usr
# Copy template
cp dracut/96luks-2fa/fido2luks.conf /etc/
# Name is optional but useful if your authenticator has a display
echo FIDO2LUKS_CREDENTIAL_ID=$(fido2luks credential [NAME]) >> /etc/fido2luks.conf
# Load config into env
set -a
. /etc/fido2luks.conf
# Repeat for each luks volume
# You can also use the `--token` flag when using LUKS2 which will then store the credential in the LUKS header,
# enabling you to use `fido2luks open-token` without passing a credential as parameter
sudo -E fido2luks -i add-key /dev/disk/by-uuid/<DISK_UUID>
# Test(only works if the luks container isn't active)
sudo -E fido2luks -i open /dev/disk/by-uuid/<DISK_UUID> luks-<DISK_UUID>
```
### Dracut
```
cd dracut
sudo make install
```
### Grub
Add `rd.luks.2fa=<CREDENTIAL_ID>:<DISK_UUID>` to `GRUB_CMDLINE_LINUX` in /etc/default/grub
Note: This is only required for your root disk, systemd will try to unlock all other LUKS partions using the same key if you added it using `fido2luks add-key`
```
grub2-mkconfig > /boot/grub2/grub.cfg
```
I'd also recommend to copy the executable onto /boot so that it is accessible in case you have to access your disk from a rescue system
```
mkdir /boot/fido2luks/
cp /usr/bin/fido2luks /boot/fido2luks/
cp /etc/fido2luks.conf /boot/fido2luks/
```
## Testing
Just reboot and see if it works, if that's the case you should remove your old less secure password from your LUKS header:
```
# Recommend in case you lose your authenticator, store this backupfile somewhere safe
cryptsetup luksHeaderBackup /dev/disk/by-uuid/<DISK_UUID> --header-backup-file luks_backup_<DISK_UUID>
# There is no turning back if you mess this up, make sure you made a backup
# You can also pass `--token` if you're using LUKS2 which will then store the credential in the LUKS header,
# which will enable you to use `fido2luks open-token` without passing a credential as parameter
fido2luks -i add-key --exclusive /dev/disk/by-uuid/<DISK_UUID>
```
## Addtional settings
### Password less
Remove your previous secret as described in the next section, in case you've already added one.
Open `/etc/fido2luks.conf` and replace `FIDO2LUKS_SALT=Ask` with `FIDO2LUKS_SALT=string:<YOUR_RANDOM_STRING>`
but be warned that this password will be included to into your initramfs.
Import the new config into env:
```
set -a
. /etc/fido2luks.conf
```
Then add the new secret to each device and update dracut afterwards `dracut -f`
### Multiple keys
Additional/backup keys are supported, Multiple fido2luks credentials can be added to your /etc/fido2luks.conf file. Credential tokens are comma separated.
```
FIDO2LUKS_CREDENTIAL_ID=<CREDENTIAL1>,<CREDENTIAL2>,<CREDENTIAL3>
```
## Removal
Remove `rd.luks.2fa` from `GRUB_CMDLINE_LINUX` in /etc/default/grub
```
set -a
. fido2luks.conf
sudo -E fido2luks -i replace-key /dev/disk/by-uuid/<DISK_UUID>
sudo rm -rf /usr/lib/dracut/modules.d/96luks-2fa /etc/dracut.conf.d/luks-2fa.conf /etc/fido2luks.conf
```
## Theory of operation
fido2luks builds on two basic building blocks, LUKS as an abstraction over linux disk encryption and and the FIDO2 extension [`hmac-secret`](https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-client-to-authenticator-protocol-v2.0-rd-20180702.html#sctn-hmac-secret-extension).
The `hmac-secret` extension allows for an secret to be dervied on the FIDO2 device from two inputs, the user supplied salt/password/keyfile and another secret contained within the FID2 device. The output of the `hmac-secret` function will then be used to decrypt the LUKS header which in turn is used to decrypt the disk.
```
+-------------------------------------------------------------------------------+
| |
| +-----------------------------------------+ |
| | FIDO2 device | |
| | | |
| | | |
+-------+--------+ +------+ | +---------------+ | | +------------------------+
| Salt/Password +-> |sha256+------------------------> | | | v | LUKS header |
+----------------+ +------+ | | | | | | +---------------+
| | | | +--------+ +------------------------+--------> |Disk master key|
| | sha256_hmac +---------> | sha256 +-------> | Keyslot 1 | +---------------+
+----------------+ | +----------+ | | | +--------+ +------------------------+
| FIDO credential+---------------> |Credential| +----> | | | | Keyslot 2 |
+----------------+ | |secret | | | | +------------------------+
| +----------+ +---------------+ |
| |
| |
+-----------------------------------------+
```
Since all these components build upon each other losing or damaging just one of them will render the disk undecryptable, it's threfore of paramount importance to backup the LUKS header and ideally set an backup password
or utilise more than one FIDO2 device. Each additional credential and password combination will require it's own LUKS keyslot since the credential secret is randomly generated for each new credential and will thus result
in a completly different secret.
## License
Licensed under
* Mozilla Public License 2.0, ([LICENSE-MPL](LICENSE-MPL) or https://www.mozilla.org/en-US/MPL/2.0/)
### Contribution
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you, as defined in the MPL 2.0
license, shall be licensed as above, without any additional terms or
conditions.
## 0.3.0-alpha
This is just the program itself, all intitrid scripts are mostly taylored to the latest 0.2.x version and will most likely not work with 0.3.0 due to breaking changes in the CLI interface.
I've decided it release the version in this state since I just do not have the time now or in the forseeable future to tewak all scripts since it's quite an tedious tasks which involves rebooting VMs countless times.
If you're interested to adapt or write scripts for an particular distro I'd be more than happy to accept pull requests.

View File

@ -1,7 +1,6 @@
#![allow(warnings)]
#[macro_use]
extern crate failure;
extern crate ctap_hmac as ctap;
#[path = "src/cli_args/mod.rs"]
mod cli_args;
@ -12,13 +11,22 @@ mod util;
use cli_args::Args;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use structopt::clap::Shell;
use structopt::StructOpt;
fn main() {
let env_outdir = env::var_os("OUT_DIR").unwrap();
let outdir = PathBuf::from(PathBuf::from(env_outdir).ancestors().nth(3).unwrap());
fs::create_dir_all(&outdir).unwrap();
// generate completion scripts, zsh does panic for some reason
for shell in Shell::variants().iter().filter(|shell| **shell != "zsh") {
Args::clap().gen_completions(env!("CARGO_PKG_NAME"), Shell::from_str(shell).unwrap(), ".");
Args::clap().gen_completions(
env!("CARGO_PKG_NAME"),
Shell::from_str(shell).unwrap(),
&outdir,
);
}
}

42
flake.lock generated
View File

@ -7,26 +7,26 @@
]
},
"locked": {
"lastModified": 1639947939,
"narHash": "sha256-pGsM8haJadVP80GFq4xhnSpNitYNQpaXk4cnA796Cso=",
"owner": "nmattia",
"lastModified": 1698420672,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"owner": "nix-community",
"repo": "naersk",
"rev": "2fc8ce9d3c025d59fee349c1f80be9785049d653",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"type": "github"
},
"original": {
"owner": "nmattia",
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1638109994,
"narHash": "sha256-OpA37PTiPMIqoRJbufbl5rOLII7HeeGcA0yl7FoyCIE=",
"lastModified": 1705496572,
"narHash": "sha256-rPIe9G5EBLXdBdn9ilGc0nq082lzQd0xGGe092R/5QE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a284564b7f75ac4db73607db02076e8da9d42c9d",
"rev": "842d9d80cfd4560648c785f8a4e6f3b096790e19",
"type": "github"
},
"original": {
@ -41,13 +41,31 @@
"utils": "utils"
}
},
"utils": {
"systems": {
"locked": {
"lastModified": 1638122382,
"narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=",
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "74f7e4319258e287b0f9cb95426c9853b282730b",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {

View File

@ -4,7 +4,7 @@
inputs = {
utils.url = "github:numtide/flake-utils";
naersk = {
url = "github:nmattia/naersk";
url = "github:nix-community/naersk";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@ -16,17 +16,17 @@
forPkgs = pkgs:
let
naersk-lib = naersk.lib."${pkgs.system}";
buildInputs = with pkgs; [ cryptsetup ];
LIBCLANG_PATH = "${pkgs.clang.cc.lib}/lib";
buildInputs = with pkgs; [ cryptsetup cryptsetup.dev udev.dev ];
nativeBuildInputs = with pkgs; [
pkgconfig
rustPlatform.bindgenHook
pkg-config
clang
];
in
rec {
# `nix build`
packages.${pname} = naersk-lib.buildPackage {
inherit pname root buildInputs nativeBuildInputs LIBCLANG_PATH;
inherit pname root buildInputs nativeBuildInputs;
};
defaultPackage = packages.${pname};
@ -46,10 +46,12 @@
'';
};
hydraJobs = checks // packages;
# `nix develop`
devShell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ rustc cargo rustfmt nixpkgs-fmt ] ++ nativeBuildInputs;
inherit buildInputs LIBCLANG_PATH;
inherit buildInputs;
};
};
forSystem = system: forPkgs nixpkgs.legacyPackages."${system}";

View File

@ -0,0 +1,15 @@
FROM rust:bullseye
RUN cargo install -f cargo-deb --debug --version 1.30.0
ARG DEBIAN_FRONTEND=noninteractive
RUN apt update && apt install -y cryptsetup pkg-config libclang-dev libcryptsetup-dev && mkdir -p /build/fido2luks
WORKDIR /build/fido2luks
ENV CARGO_TARGET_DIR=/build/fido2luks/target
RUN cargo install fido2luks -f
CMD bash -xc 'cp -rf /code/* /build/fido2luks && cargo-deb && cp target/debian/*.deb /out'

9
initramfs-tools/build-deb.sh Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -ex
docker build . -t fido2luks-deb
mkdir -p debs
docker run -ti -v "$(pwd)/..:/code:ro" -v "$(pwd)/debs:/out" fido2luks-deb

View File

@ -1,5 +1,3 @@
FIDO2LUKS_SALT=Ask
#FIDO2LUKS_PASSWORD_HELPER="/usr/bin/plymouth ask-for-password --prompt 'FIDO2 password salt'"
FIDO2LUKS_CREDENTIAL_ID=
FIDO2LUKS_USE_TOKEN=0
FIDO2LUKS_PASSWORD_FALLBACK=1

View File

@ -2,29 +2,9 @@
set -a
. /etc/fido2luks.conf
# Set Defaults
if [ -z "$FIDO2LUKS_USE_TOKEN" ]; then
FIDO2LUKS_USE_TOKEN=0
fi
if [ -z "$FIDO2LUKS_PASSWORD_FALLBACK" ]; then
FIDO2LUKS_PASSWORD_FALLBACK=1
fi
if [ -z "$FIDO2LUKS_PASSWORD_HELPER" ]; then
MSG="FIDO2 password salt for $CRYPTTAB_NAME"
export FIDO2LUKS_PASSWORD_HELPER="plymouth ask-for-password --prompt '$MSG'"
fi
if [ "$FIDO2LUKS_USE_TOKEN" -eq 1 ]; then
export FIDO2LUKS_CREDENTIAL_ID="$FIDO2LUKS_CREDENTIAL_ID,$(fido2luks token list --csv $CRYPTTAB_SOURCE)"
fi
fido2luks print-secret --bin
# Fall back to passphrase-based unlock if fido2luks fails
if [ "$?" -gt 0 ] && [ "$FIDO2LUKS_PASSWORD_FALLBACK" -eq 1 ]; then
plymouth ask-for-password --prompt "Password for $CRYPTTAB_SOURCE"
fi
fido2luks print-secret --bin "$CRYPTTAB_SOURCE" $([ "$FIDO2LUKS_USE_TOKEN" -eq 0 ] && printf "--disable-token")

View File

@ -2,33 +2,22 @@ use crate::error::*;
use crate::luks::{Fido2LuksToken, LuksDevice};
use crate::util::sha256;
use crate::*;
pub use cli_args::Args;
use cli_args::*;
use ctap_hid_fido2::public_key_credential_descriptor::PublicKeyCredentialDescriptor;
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::time::Duration;
use std::time::SystemTime;
use structopt::clap::Shell;
use structopt::StructOpt;
use ctap::{FidoCredential, FidoErrorKind};
use std::io::{Read, Write};
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;
pub use cli_args::Args;
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(
@ -36,35 +25,129 @@ fn derive_secret(
salt: &[u8; 32],
timeout: u64,
pin: Option<&str>,
) -> Fido2LuksResult<([u8; 32], FidoCredential)> {
) -> Fido2LuksResult<([u8; 32], PublicKeyCredentialDescriptor)> {
if credentials.is_empty() {
return Err(Fido2LuksError::InsufficientCredentials);
}
let timeout = Duration::from_secs(timeout);
let start = SystemTime::now();
while let Ok(el) = start.elapsed() {
if el > timeout {
return Err(error::Fido2LuksError::NoAuthenticatorError);
}
if get_devices()
.map(|devices| !devices.is_empty())
.unwrap_or(false)
{
break;
}
thread::sleep(Duration::from_millis(500));
}
//while let Ok(el) = start.elapsed() {
// if el > timeout {
// return Err(error::Fido2LuksError::NoAuthenticatorError);
// }
// if get_devices()
// .map(|devices| !devices.is_empty())
// .unwrap_or(false)
// {
// break;
// }
// thread::sleep(Duration::from_millis(500));
//}
let credentials = credentials
.iter()
.map(|hex| FidoCredential {
.map(|hex| PublicKeyCredentialDescriptor {
id: hex.0.clone(),
public_key: None,
ctype: Default::default(),
})
.collect::<Vec<_>>();
let credentials = credentials.iter().collect::<Vec<_>>();
let (unsalted, cred) =
perform_challenge(&credentials, salt, timeout - start.elapsed().unwrap(), pin)?;
Ok((sha256(&[salt, &unsalted[..]]), cred.clone()))
let binary = sha256(&[salt, &unsalted[..]]);
Ok((binary, cred.clone()))
}
pub fn extend_creds_device(
creds: &[HexEncoded],
luks_dev: &mut LuksDevice,
) -> Fido2LuksResult<Vec<HexEncoded>> {
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()).map_err(|_e| {
Fido2LuksError::HexEncodingError {
string: cred.clone(),
}
})?;
additional.insert(parsed);
}
}
Ok(Vec::from_iter(additional.into_iter()))
}
pub fn get_input(
secret: &SecretParameters,
authenticator: &AuthenticatorParameters,
interactive: bool,
q: &str,
verify: bool,
) -> Fido2LuksResult<(Option<String>, [u8; 32])> {
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());
let password = match pin {
Some(ref pin) if read.len() > pin.len() => {
read.chars().skip(pin.len() + 1).collect::<String>()
}
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 {
@ -80,6 +163,11 @@ pub fn prompt_interaction(interactive: bool) {
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 {
@ -87,13 +175,14 @@ 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(Some(name.as_ref()), pin)?;
let cred =
make_credential_id(Some(name.as_str()).filter(|name| name.len() > 0), pin, &[])?;
println!("{}", hex::encode(&cred.id));
Ok(())
}
@ -102,26 +191,44 @@ pub fn run_cli() -> Fido2LuksResult<()> {
authenticator,
credentials,
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 salt = if interactive || secret.password_helper == PasswordHelper::Stdin {
util::read_password_hashed("Password", false)
} else {
secret.salt.obtain_sha256(&secret.password_helper)
}?;
log(&|| {
format!(
"credentials: {}",
credentials
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)
});
prompt_interaction(interactive);
let (secret, _cred) = derive_secret(
credentials.ids.0.as_slice(),
let (secret, cred) = derive_secret(
&credentials,
&salt,
authenticator.await_time,
pin,
pin.as_deref(),
)?;
log(&|| format!("credential used: {}", hex::encode(&cred.id)));
if *binary {
stdout.write_all(&secret[..])?;
} else {
@ -136,7 +243,6 @@ pub fn run_cli() -> Fido2LuksResult<()> {
secret,
luks_mod,
existing_secret: other_secret,
token,
..
}
| Command::ReplaceKey {
@ -146,24 +252,47 @@ pub fn run_cli() -> Fido2LuksResult<()> {
secret,
luks_mod,
replacement: other_secret,
token,
..
} => {
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>)> {
-> Fido2LuksResult<(
Vec<u8>,
Option<PublicKeyCredentialDescriptor>,
)> {
match other_secret {
OtherSecret {
keyfile: Some(file),
@ -172,41 +301,87 @@ pub fn run_cli() -> Fido2LuksResult<()> {
OtherSecret {
fido_device: true, ..
} => {
let (pin, salt) = inputs(salt_q, verify)?;
prompt_interaction(interactive);
Ok(derive_secret(
&credentials.ids.0,
&salt(salt_q, verify)?,
&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 = |q: &str, verify: bool| -> Fido2LuksResult<([u8; 32], FidoCredential)> {
let secret =
|q: &str,
verify: bool,
credentials: &[HexEncoded]|
-> Fido2LuksResult<([u8; 32], PublicKeyCredentialDescriptor)> {
let (pin, salt) = inputs(q, verify)?;
prompt_interaction(interactive);
derive_secret(
&credentials.ids.0,
&salt(q, verify)?,
authenticator.await_time,
pin.as_deref(),
)
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, .. } => {
let (existing_secret, _) = other_secret("Current password", false)?;
let (new_secret, cred) = secret("Password to be added", true)?;
Command::AddKey {
exclusive,
generate_credential,
comment,
..
} => {
let (existing_secret, existing_credential) =
other_secret("Current password", false)?;
let excluded_credential = existing_credential.as_ref();
let exclude_list = excluded_credential
.as_ref()
.map(core::slice::from_ref)
.unwrap_or_default();
existing_credential.iter().for_each(|cred| {
log(&|| {
format!(
"using credential to unlock container: {}",
hex::encode(&cred.id)
)
})
});
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(),
dbg!(exclude_list),
)?;
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),
comment.as_deref().map(String::from),
)?;
if *exclusive {
let destroyed = luks_dev.remove_keyslots(&[added_slot])?;
@ -225,24 +400,43 @@ pub fn run_cli() -> Fido2LuksResult<()> {
}
Ok(())
}
Command::ReplaceKey { add_password, .. } => {
let (existing_secret, _) = secret("Current password", 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[..]),
None,
)
} 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(),
@ -258,80 +452,88 @@ pub fn run_cli() -> Fido2LuksResult<()> {
authenticator,
secret,
name,
credentials,
retries,
dry_run,
allow_discards,
..
}
| Command::OpenToken {
luks,
authenticator,
secret,
name,
retries,
allow_discards,
} => {
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, *allow_discards)
}),
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, *allow_discards)
})
} 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,
*dry_run,
*allow_discards,
),
_ => unreachable!(),
)
} 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 => {}
Fido2LuksError::AuthenticatorError { ref cause }
if cause.kind() == FidoErrorKind::Timeout && retries > 0 => {}
//Fido2LuksError::AuthenticatorError { ref cause }
// 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(());
}
}
}
}
@ -363,8 +565,13 @@ pub fn run_cli() -> Fido2LuksResult<()> {
continue;
}
println!(
"{}:\n\tSlots: {}\n\tCredentials: {}",
"{}{}:\n\tSlots: {}\n\tCredentials: {}",
id,
token
.comment
.as_deref()
.map(|comment| format!(" - {}", comment))
.unwrap_or_default(),
if token.keyslots.is_empty() {
"None".into()
} else {
@ -390,6 +597,7 @@ pub fn run_cli() -> Fido2LuksResult<()> {
TokenCommand::Add {
device,
credentials,
comment,
slot,
} => {
let mut dev = LuksDevice::load(device)?;
@ -401,7 +609,11 @@ 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,
comment.as_deref().map(String::from),
))?;
1
} else {
tokens.len()
@ -409,7 +621,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);
@ -437,7 +649,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)?;
}
@ -467,16 +679,64 @@ pub fn run_cli() -> Fido2LuksResult<()> {
}
},
Command::GenerateCompletions { shell, 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"),
match shell.as_ref() {
"bash" => Shell::Bash,
"fish" => Shell::Fish,
_ => unreachable!("structopt shouldn't allow us to reach this point"),
},
Shell::from_str(variant)
.expect("structopt shouldn't allow us to reach this point"),
&out_dir,
);
}
Ok(())
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_read_password_pin_prefixed() {
// 1234:test -> PIN: 1234, password: test
assert_eq!(
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".into())).unwrap(),
(None, util::sha256(&["test".as_bytes()]))
);
// 1234::test -> PIN: 1234, password: :test
assert_eq!(
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()]))
);
}
}

View File

@ -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_sha256(&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,

View File

@ -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>);
@ -58,19 +65,24 @@ impl<T: Display + FromStr> FromStr for CommaSeparated<T> {
#[derive(Debug, StructOpt)]
pub struct Credentials {
/// FIDO credential ids, separated by ',' generate using fido2luks credential
#[structopt(name = "credential-id", env = "FIDO2LUKS_CREDENTIAL_ID")]
pub ids: CommaSeparated<HexEncoded>,
#[structopt(
name = "credential-ids",
env = "FIDO2LUKS_CREDENTIAL_ID",
short = "c",
long = "creds"
)]
pub ids: Option<CommaSeparated<HexEncoded>>,
}
#[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,
/// Location to read PIN from
#[structopt(long = "pin-source", env = "FIDO2LUKS_PIN_SOURCE")]
pub pin_source: Option<PathBuf>,
/// Request PIN and password combined `pin:password` when using an password helper
#[structopt(long = "pin-prefixed")]
pub pin_prefixed: bool,
/// Await for an authenticator to be connected, timeout after n seconds
#[structopt(
@ -90,13 +102,20 @@ pub struct LuksParameters {
/// Try to unlock the device using a specifc keyslot, ignore all other slots
#[structopt(long = "slot", env = "FIDO2LUKS_DEVICE_SLOT")]
pub slot: Option<u32>,
/// Disable implicit use of LUKS2 tokens
#[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,
}
#[derive(Debug, StructOpt, Clone)]
pub struct LuksModParameters {
/// Number of milliseconds required to derive the volume decryption key
/// Defaults to 10ms when using an authenticator or the default by cryptsetup when using a password
#[structopt(long = "kdf-time", name = "kdf-time")]
#[structopt(long = "kdf-time", name = "kdf-time", env = "FIDO2LUKS_KDF_TIME")]
pub kdf_time: Option<u64>,
}
@ -119,15 +138,17 @@ pub struct SecretParameters {
#[structopt(
name = "password-helper",
env = "FIDO2LUKS_PASSWORD_HELPER",
default_value = "/usr/bin/env systemd-ask-password 'Please enter second factor for LUKS disk encryption!'"
long = "password-helper"
)]
pub password_helper: PasswordHelper,
pub password_helper: Option<PasswordHelper>,
}
#[derive(Debug, StructOpt)]
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,
}
@ -138,7 +159,7 @@ pub struct OtherSecret {
#[structopt(short = "d", long = "keyfile", conflicts_with = "fido_device")]
pub keyfile: Option<PathBuf>,
/// Use another fido device instead of a password
/// Note: this requires for the credential fot the other device to be passed as argument as well
/// Note: this requires for the credential for the other device to be passed as argument as well
#[structopt(short = "f", long = "fido-device", conflicts_with = "keyfile")]
pub fido_device: bool,
}
@ -147,8 +168,9 @@ pub struct OtherSecret {
pub enum Command {
#[structopt(name = "print-secret")]
PrintSecret {
// version 0.3.0 will store use the lower case ascii encoded hex string making binary output unnecessary
/// Prints the secret as binary instead of hex encoded
#[structopt(short = "b", long = "bin")]
#[structopt(hidden = true, short = "b", long = "bin")]
binary: bool,
#[structopt(flatten)]
credentials: Credentials,
@ -156,6 +178,9 @@ pub enum Command {
authenticator: AuthenticatorParameters,
#[structopt(flatten)]
secret: SecretParameters,
/// Load credentials from LUKS header
#[structopt(env = "FIDO2LUKS_DEVICE")]
device: Option<PathBuf>,
},
/// Adds a generated key to the specified LUKS device
#[structopt(name = "add-key")]
@ -164,6 +189,9 @@ pub enum Command {
luks: LuksParameters,
#[structopt(flatten)]
credentials: Credentials,
/// Comment to be associated with this credential
#[structopt(long = "comment")]
comment: Option<String>,
#[structopt(flatten)]
authenticator: AuthenticatorParameters,
#[structopt(flatten)]
@ -171,9 +199,9 @@ pub enum Command {
/// Will wipe all other keys
#[structopt(short = "e", long = "exclusive")]
exclusive: bool,
/// Will add an token to your LUKS 2 header, including the credential id
#[structopt(short = "t", long = "token")]
token: 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)]
@ -193,16 +221,16 @@ pub enum Command {
/// Add the password and keep the key
#[structopt(short = "a", long = "add-password")]
add_password: bool,
/// Will add an token to your LUKS 2 header, including the credential id
#[structopt(short = "t", long = "token")]
token: bool,
/// Remove the affected credential from LUKS header
#[structopt(short = "r", long = "remove-cred")]
remove_cred: bool,
#[structopt(flatten)]
replacement: OtherSecret,
#[structopt(flatten)]
luks_mod: LuksModParameters,
},
/// Open the LUKS device
#[structopt(name = "open")]
#[structopt(name = "open", alias = "open-token")]
Open {
#[structopt(flatten)]
luks: LuksParameters,
@ -216,23 +244,9 @@ pub enum Command {
secret: SecretParameters,
#[structopt(short = "r", long = "max-retries", default_value = "0")]
retries: i32,
/// Pass SSD trim instructions to the underlying block device
#[structopt(long = "allow-discards")]
allow_discards: bool,
},
/// Open the LUKS device using credentials embedded in the LUKS 2 header
#[structopt(name = "open-token")]
OpenToken {
#[structopt(flatten)]
luks: LuksParameters,
#[structopt(env = "FIDO2LUKS_MAPPER_NAME")]
name: String,
#[structopt(flatten)]
authenticator: AuthenticatorParameters,
#[structopt(flatten)]
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,
/// Pass SSD trim instructions to the underlying block device
#[structopt(long = "allow-discards")]
allow_discards: bool,
@ -243,7 +257,7 @@ pub enum Command {
#[structopt(flatten)]
authenticator: AuthenticatorParameters,
/// Name to be displayed on the authenticator display
#[structopt(env = "FIDO2LUKS_CREDENTIAL_NAME", default_value = "fido2luks")]
#[structopt(env = "FIDO2LUKS_CREDENTIAL_NAME", default_value = "")]
name: String,
},
/// Check if an authenticator is connected
@ -251,11 +265,12 @@ pub enum Command {
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,
},
}
@ -275,8 +290,17 @@ 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>,
/// Comment to be associated with this credential
#[structopt(long = "comment")]
comment: Option<String>,
/// Slot to which the credentials will be added
#[structopt(long = "slot", env = "FIDO2LUKS_DEVICE_SLOT")]
slot: u32,
@ -285,8 +309,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>,

View File

@ -1,73 +1,133 @@
use crate::error::*;
use crate::util;
use ctap::{
self, extensions::hmac::HmacExtension, request_multiple_devices, FidoAssertionRequestBuilder,
FidoCredential, FidoCredentialRequestBuilder, FidoDevice, FidoError, FidoErrorKind,
};
use ctap_hid_fido2;
use ctap_hid_fido2::fidokey::get_assertion::get_assertion_params;
use ctap_hid_fido2::fidokey::make_credential::make_credential_params;
use ctap_hid_fido2::fidokey::GetAssertionArgsBuilder;
use ctap_hid_fido2::fidokey::MakeCredentialArgsBuilder;
use ctap_hid_fido2::get_fidokey_devices;
use ctap_hid_fido2::public_key_credential_descriptor::PublicKeyCredentialDescriptor;
use ctap_hid_fido2::public_key_credential_user_entity::PublicKeyCredentialUserEntity;
use ctap_hid_fido2::FidoKeyHidFactory;
use ctap_hid_fido2::HidInfo;
use ctap_hid_fido2::LibCfg;
use std::time::Duration;
const RP_ID: &str = "fido2luks";
fn lib_cfg() -> LibCfg {
let mut cfg = LibCfg::init();
cfg.enable_log = false;
cfg.keep_alive_msg = String::new();
cfg
}
pub fn make_credential_id(
name: Option<&str>,
pin: Option<&str>,
) -> Fido2LuksResult<FidoCredential> {
let mut request = FidoCredentialRequestBuilder::default().rp_id(RP_ID);
if let Some(user_name) = name {
request = request.user_name(user_name);
}
let request = request.build().unwrap();
let make_credential = |device: &mut FidoDevice| {
exclude: &[&PublicKeyCredentialDescriptor],
) -> Fido2LuksResult<PublicKeyCredentialDescriptor> {
let mut req = MakeCredentialArgsBuilder::new(RP_ID, &[])
.extensions(&[make_credential_params::Extension::HmacSecret(Some(true))]);
if let Some(pin) = pin {
device.unlock(pin)?;
req = req.pin(pin);
} else {
req = req.without_pin_and_uv();
}
device.make_hmac_credential(&request)
};
Ok(request_multiple_devices(
get_devices()?
.iter_mut()
.map(|device| (device, &make_credential)),
None,
)?)
for cred in exclude {
req = req.exclude_authenticator(cred.id.as_ref());
}
if let Some(_) = name {
req = req.user_entity(&PublicKeyCredentialUserEntity::new(
Some(b"00"),
name.clone(),
name,
));
}
let devices = get_devices()?;
let mut err: Option<Fido2LuksError> = None;
let req = req.build();
for dev in devices {
let handle = FidoKeyHidFactory::create_by_params(&vec![dev.param], &lib_cfg()).unwrap();
match handle.make_credential_with_args(&req) {
Ok(resp) => return Ok(resp.credential_descriptor),
Err(e) => err = Some(e.into()),
}
}
Err(err.unwrap_or(Fido2LuksError::NoAuthenticatorError))
}
pub fn perform_challenge<'a>(
credentials: &'a [&'a FidoCredential],
credentials: &'a [&'a PublicKeyCredentialDescriptor],
salt: &[u8; 32],
timeout: Duration,
_timeout: Duration,
pin: Option<&str>,
) -> Fido2LuksResult<([u8; 32], &'a FidoCredential)> {
let request = FidoAssertionRequestBuilder::default()
.rp_id(RP_ID)
.credentials(credentials)
.build()
.unwrap();
let get_assertion = |device: &mut FidoDevice| {
if let Some(pin) = pin {
device.unlock(pin)?;
) -> Fido2LuksResult<([u8; 32], &'a PublicKeyCredentialDescriptor)> {
if credentials.is_empty() {
return Err(Fido2LuksError::InsufficientCredentials);
}
device.get_hmac_assertion(&request, &util::sha256(&[&salt[..]]), None)
let mut req = GetAssertionArgsBuilder::new(RP_ID, &[]).extensions(&[
get_assertion_params::Extension::HmacSecret(Some(util::sha256(&[&salt[..]]))),
]);
for cred in credentials {
req = req.add_credential_id(&cred.id);
}
if let Some(pin) = pin {
req = req.pin(pin);
} else {
req = req.without_pin_and_uv();
}
let process_response = |resp: Vec<get_assertion_params::Assertion>| -> Fido2LuksResult<([u8; 32], &'a PublicKeyCredentialDescriptor)> {
for att in resp {
for ext in att.extensions.iter() {
match ext {
get_assertion_params::Extension::HmacSecret(Some(secret)) => {
//TODO: eliminate unwrap
let cred_used = credentials
.iter()
.copied()
.find(|cred| {
att.credential_id == cred.id
})
.unwrap();
return Ok((secret.clone(), cred_used));
}
_ => continue,
}
}
}
Err(Fido2LuksError::WrongSecret)
};
let (credential, (secret, _)) = request_multiple_devices(
get_devices()?
.iter_mut()
.map(|device| (device, &get_assertion)),
Some(timeout),
)?;
Ok((secret, credential))
let devices = get_devices()?;
let mut err: Option<Fido2LuksError> = None;
let req = req.build();
for dev in devices {
let handle = FidoKeyHidFactory::create_by_params(&vec![dev.param], &lib_cfg()).unwrap();
match handle.get_assertion_with_args(&req) {
Ok(resp) => return process_response(resp),
Err(e) => err = Some(e.into()),
}
}
Err(err.unwrap_or(Fido2LuksError::NoAuthenticatorError))
}
pub fn get_devices() -> Fido2LuksResult<Vec<FidoDevice>> {
let mut devices = Vec::with_capacity(2);
for di in ctap::get_devices()? {
match FidoDevice::new(&di) {
Err(e) => match e.kind() {
FidoErrorKind::ParseCtap | FidoErrorKind::DeviceUnsupported => (),
err => return Err(FidoError::from(err).into()),
},
Ok(dev) => devices.push(dev),
pub fn may_require_pin() -> Fido2LuksResult<bool> {
for dev in get_devices()? {
let handle = FidoKeyHidFactory::create_by_params(&vec![dev.param], &lib_cfg()).unwrap();
let info = handle.get_info()?;
let needs_pin = info
.options
.iter()
.any(|(name, val)| &name[..] == "clientPin" && *val);
if needs_pin {
return Ok(true);
}
}
Ok(devices)
Ok(false)
}
pub fn get_devices() -> Fido2LuksResult<Vec<HidInfo>> {
Ok(get_fidokey_devices())
}

View File

@ -1,4 +1,4 @@
use ctap::FidoError;
use anyhow;
use libcryptsetup_rs::LibcryptErr;
use std::io;
use std::io::ErrorKind;
@ -14,7 +14,7 @@ pub enum Fido2LuksError {
#[fail(display = "unable to read keyfile: {}", cause)]
KeyfileError { cause: io::Error },
#[fail(display = "authenticator error: {}", cause)]
AuthenticatorError { cause: ctap::FidoError },
AuthenticatorError { cause: anyhow::Error },
#[fail(display = "no authenticator found, please ensure your device is plugged in")]
NoAuthenticatorError,
#[fail(display = " {}", cause)]
@ -29,6 +29,16 @@ 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 From<anyhow::Error> for Fido2LuksError {
fn from(cause: anyhow::Error) -> Self {
Fido2LuksError::AuthenticatorError { cause }
}
}
impl Fido2LuksError {
@ -50,6 +60,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)]
@ -85,12 +97,6 @@ impl From<LuksError> for Fido2LuksError {
}
}
impl From<FidoError> for Fido2LuksError {
fn from(e: FidoError) -> Self {
AuthenticatorError { cause: e }
}
}
impl From<LibcryptErr> for Fido2LuksError {
fn from(e: LibcryptErr) -> Self {
match e {

View File

@ -1,9 +1,8 @@
use crate::error::*;
use libcryptsetup_rs::{
CryptActivateFlag, CryptActivateFlags, CryptDevice, CryptInit, CryptTokenInfo,
EncryptionFormat, KeyslotInfo, TokenInput,
};
use libcryptsetup_rs::consts::flags::CryptActivate;
use libcryptsetup_rs::consts::vals::{EncryptionFormat, KeyslotInfo};
use libcryptsetup_rs::{CryptDevice, CryptInit, CryptTokenInfo, TokenInput};
use std::collections::{HashMap, HashSet};
use std::path::Path;
@ -111,6 +110,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
@ -128,6 +141,7 @@ impl LuksDevice {
old_secret: &[u8],
iteration_time: Option<u64>,
credential_id: Option<&[u8]>,
comment: Option<String>,
) -> Fido2LuksResult<u32> {
if let Some(millis) = iteration_time {
self.device.settings_handle().set_iteration_time(millis)
@ -138,7 +152,7 @@ impl LuksDevice {
.add_by_passphrase(None, old_secret, secret)?;
if let Some(id) = credential_id {
self.device.token_handle().json_set(TokenInput::AddToken(
&serde_json::to_value(&Fido2LuksToken::new(id, slot)).unwrap(),
&serde_json::to_value(&Fido2LuksToken::new(id, slot, comment)).unwrap(),
))?;
}
@ -190,9 +204,11 @@ impl LuksDevice {
None,
None,
old_secret,
CryptActivateFlags::empty(),
CryptActivate::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,
@ -200,9 +216,18 @@ impl LuksDevice {
)? as u32;
if let Some(id) = credential_id {
if self.is_luks2()? {
let token = self.find_token(slot)?.map(|(t, _)| t);
let json = serde_json::to_value(&Fido2LuksToken::new(id, slot)).unwrap();
if let Some(token) = token {
let (token_id, token_data) = match self.find_token(slot)? {
Some((id, data)) => (Some(id), Some(data)),
_ => (None, None),
};
let json = serde_json::to_value(&Fido2LuksToken::new(
id,
slot,
// retain comment on replace
token_data.map(|data| data.comment).flatten(),
))
.unwrap();
if let Some(token) = token_id {
self.device
.token_handle()
.json_set(TokenInput::ReplaceToken(token, &json))?;
@ -221,15 +246,16 @@ impl LuksDevice {
name: &str,
secret: &[u8],
slot_hint: Option<u32>,
dry_run: bool,
allow_discard: bool,
) -> Fido2LuksResult<u32> {
let mut flags = CryptActivateFlags::empty();
let mut flags = CryptActivate::empty();
if allow_discard {
flags = CryptActivateFlags::new(vec![CryptActivateFlag::AllowDiscards]);
flags = flags | CryptActivate::ALLOW_DISCARDS;
}
self.device
.activate_handle()
.activate_by_passphrase(Some(name), slot_hint, secret, flags)
.activate_by_passphrase(Some(name).filter(|_| !dry_run), slot_hint, secret, flags)
.map_err(LuksError::activate)
}
@ -238,6 +264,7 @@ impl LuksDevice {
name: &str,
secret: impl Fn(Vec<String>) -> Fido2LuksResult<([u8; 32], String)>,
slot_hint: Option<u32>,
dry_run: bool,
allow_discard: bool,
) -> Fido2LuksResult<u32> {
if !self.is_luks2()? {
@ -282,7 +309,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, allow_discard) {
match self.activate(name, &secret, slot, dry_run, allow_discard) {
Err(Fido2LuksError::WrongSecret) => (),
res => return res,
}
@ -297,16 +324,19 @@ pub struct Fido2LuksToken {
pub type_: String,
pub credential: HashSet<String>,
pub keyslots: HashSet<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}
impl Fido2LuksToken {
pub fn new(credential_id: impl AsRef<[u8]>, slot: u32) -> Self {
Self::with_credentials(std::iter::once(credential_id), slot)
pub fn new(credential_id: impl AsRef<[u8]>, slot: u32, comment: Option<String>) -> Self {
Self::with_credentials(std::iter::once(credential_id), slot, comment)
}
pub fn with_credentials<I: IntoIterator<Item = B>, B: AsRef<[u8]>>(
credentials: I,
slot: u32,
comment: Option<String>,
) -> Self {
Self {
credential: credentials
@ -314,6 +344,7 @@ impl Fido2LuksToken {
.map(|cred| hex::encode(cred.as_ref()))
.collect(),
keyslots: vec![slot.to_string()].into_iter().collect(),
comment,
..Default::default()
}
}
@ -328,6 +359,7 @@ impl Default for Fido2LuksToken {
type_: Self::default_type().into(),
credential: HashSet::new(),
keyslots: HashSet::new(),
comment: None,
}
}
}

View File

@ -1,6 +1,5 @@
#[macro_use]
extern crate failure;
extern crate ctap_hmac as ctap;
#[macro_use]
extern crate serde_derive;
use crate::cli::*;

View File

@ -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();