Compare commits

..

108 Commits

Author SHA1 Message Date
210da1ce0f
added --allow-discards flag 2021-12-28 13:35:34 +01:00
4509cacd6d
update deps 2021-12-28 13:32:29 +01:00
cbachert
67136f2405
Add Ubuntu fallback to passphrase, add defaults (#35)
Co-authored-by: cbachert <1316659-cbachert@users.noreply.gitlab.com>
2021-09-11 16:33:14 +02:00
b2e4950db5
update ctap_hmac 2021-07-26 18:46:10 +02:00
5496c4e61b
always set credential name 2021-07-14 15:47:23 +02:00
51fa26b7d5
bump version 2021-07-14 12:24:58 +02:00
Vyacheslav Konovalov
a3696962e8
Support for initcpio (#31)
* Add initcpio hook and install script

* Make PIN optional

* Add README for initcpio

* Fix PKGBUILD, add install of initcpio

* Fix README for initcpio
2021-07-14 12:23:32 +02:00
shimunn
7e6b33ae7f
Theory of operation (#30)
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-28 12:56:57 +01:00
b3495c45f3
add nix flake 2021-02-08 16:06:56 +01:00
shimunn
17ca487b85
Obvious password promt (#29)
* obvious password promt

* prompt interaction with FIDO device
2021-02-08 15:58:41 +01:00
b0404f2fc1
minimum YubiKey firmware version
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2020-11-17 13:05:45 +01:00
shimunn
de21e3ef8d
Merge pull request #21 from aacebedo/master
Added an helper script to be used with pam_mount
2020-11-01 21:18:13 +01:00
Alexandre ACEBEDO
8a7b3addbb Added an helper script to be used with pam_mount 2020-11-01 18:21:04 +01:00
e7e44cd61b
fix test
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-13 14:17:22 +02:00
0ec859f4a6
remove trailing newline from pin
Some checks failed
continuous-integration/drone/push Build is failing
2020-09-25 12:40:34 +02:00
55bae4161e
add credentials situated in the luks header to credential list
Some checks failed
continuous-integration/drone/push Build is failing
2020-09-19 18:23:21 +02:00
086c1a0594
file path must be relative to src
Some checks failed
continuous-integration/drone/push Build is failing
2020-09-05 19:35:36 +02:00
c2e38eb06f
generate shell completions during build
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2020-09-05 18:52:07 +02:00
03ef5721e0
bump version
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2020-09-03 14:56:36 +02:00
008e644024
auto detect current version 2020-09-03 14:56:22 +02:00
e1f762ddc9
add subcommand to generate bash completions
All checks were successful
continuous-integration/drone/push Build is passing
2020-09-03 14:45:52 +02:00
Saravanan Palanisamy
2266754a95
create PKGBUILD file for archlinux package (#17)
Some checks reported errors
continuous-integration/drone/push Build encountered an error
* create PKGBUILD file

* use build & install method

* add package dependencies
2020-09-02 14:14:40 +02:00
8811cff6d1
0.2.12
All checks were successful
continuous-integration/drone/tag Build is passing
2020-08-31 00:04:24 +02:00
99787b614c
Merge branch 'pin_source' into master
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2020-08-31 00:00:42 +02:00
ee28f87148
always print the full error message
All checks were successful
continuous-integration/drone/push Build is passing
2020-08-30 17:09:57 +02:00
196356fe3b
structopt does not allow for flags to be linked to env atm 2020-08-25 21:47:25 +02:00
3ff7e698bd
add flag to read pin from alternate source
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2020-08-25 21:26:30 +02:00
04d0d60fb3
use ubuntu as base image
Some checks reported errors
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build was killed
2020-08-16 15:30:20 +02:00
e64f777c54
use sh to run password helper 2020-08-16 13:42:54 +02:00
8465949b44
spell promPt correctly 2020-08-16 13:42:35 +02:00
shimunn
06bed03e7b
Merge pull request #13 from Andrew-Finn/master
Added documentation and tweaked readme
2020-08-14 12:40:02 +02:00
Andrew-Finn
36f82e7c3a Added and edited documentation 2020-08-14 11:36:25 +01:00
cd90564f60
protect fido2luks.conf from being overwritten
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
2020-08-11 23:08:12 +02:00
0f6d79a7e4
get plymouth to display a message
Some checks failed
continuous-integration/drone/push Build is failing
2020-08-11 22:41:21 +02:00
4136b1bfad
relicense to MPL
Some checks failed
continuous-integration/drone/push Build is failing
2020-07-27 09:22:02 +02:00
81016a1a42
require MPL for new contributions
Some checks failed
continuous-integration/drone/push Build is failing
2020-07-26 21:11:03 +02:00
840868468b
cargo-deb meta
All checks were successful
continuous-integration/drone/push Build is passing
2020-07-04 21:32:21 +02:00
97880e4f41
refuse removal while crypttab depends on keyscript
All checks were successful
continuous-integration/drone/push Build is passing
2020-07-03 16:21:35 +02:00
e798ba5c70
remove keyscript aswell
All checks were successful
continuous-integration/drone/push Build is passing
2020-07-03 15:59:12 +02:00
298e05fed7
initramfs-tools
All checks were successful
continuous-integration/drone/push Build is passing
2020-07-03 15:55:23 +02:00
a498e1416f
automate cargo publish
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2020-06-23 23:55:07 +02:00
92e413de50 refactored luks operations
All checks were successful
continuous-integration/drone/push Build is passing
2020-06-22 20:47:19 +00:00
023399bb14
use unstable cryptsetup
Some checks failed
continuous-integration/drone/push Build is failing
2020-06-22 18:17:10 +02:00
a53a430c23
update drone
Some checks failed
continuous-integration/drone/push Build is failing
2020-06-21 22:16:45 +02:00
5f107cd337
add non existing token
Some checks failed
continuous-integration/drone/push Build is failing
2020-06-19 20:09:36 +02:00
ddfd24a098
ensure replace_key uses the same slot 2020-06-19 20:05:05 +02:00
743edf668a
document --token 2020-06-13 14:35:46 +02:00
4507107fac
update libcryptsetup-rs 2020-06-10 13:50:28 +02:00
a8482c50a2
handle tokens when replacing 2020-06-08 19:22:19 +02:00
09be5ef551
assemble secret in correct order 2020-06-08 18:08:24 +02:00
6f6c84ddba
skip luks2 check until underlying issue is fixed 2020-06-07 14:14:51 +02:00
5a05cad695
more precise description 2020-06-06 23:39:23 +02:00
d8d24b40f5
Merge branch 'cli_reorg' 2020-06-06 23:37:22 +02:00
c1a82b9ae6
update libcryptsetup_rs to 0.4.0 2020-06-06 22:43:18 +02:00
a26b79bcd6
reduced redundant code 2020-05-05 23:53:50 +02:00
f774580c9c
update to current api 2020-05-05 23:28:44 +02:00
69732a1ad6
restore order 2020-04-29 20:33:28 +02:00
b8ae9d91f0
rudimentary pin support 2020-04-29 19:56:18 +02:00
fcdd2a2d3d
add option to specify keyslot 2020-04-29 18:55:25 +02:00
c3d6425e2d
reorganised cli 2020-04-29 18:50:55 +02:00
0b19760175
hint slots 2020-04-28 19:09:53 +02:00
2ec8679c47
open token 2020-04-28 14:27:14 +02:00
65e1dead8b
remove token 2020-04-27 22:07:00 +02:00
478fb5e036
store luks token 2020-04-27 19:26:21 +02:00
1547f5e199
get format 2020-04-27 18:12:06 +02:00
5c0364587e
update ctap 2020-04-26 18:58:37 +02:00
9307503bdc
applied clippy lints 2020-04-07 20:06:24 +02:00
b94f45d1ff
patch secret_gen before obtaing first secret 2020-04-06 23:33:41 +02:00
c8fb636846
mention clang build dependency 2020-04-06 22:52:15 +02:00
49e2835f60
enable fido requests to be sent to multiple devices at once 2020-04-06 21:38:11 +02:00
d5c0d48f03
allow another fido device to be used as previous secret 2020-04-06 20:18:00 +02:00
ad2451f548
add timeout 2020-04-05 23:24:18 +02:00
bb7ee7c1ce
request password only once if possible 2020-04-03 22:02:05 +02:00
0ba77963d2
update ctap_hmac 2020-04-02 17:22:15 +02:00
1658800553
request_multiple 2020-04-01 20:24:49 +02:00
a394b7d1d1
libcryptsetup-rs patch 2020-03-28 14:54:36 +01:00
c99d7f562d
support luks2 2020-03-27 20:08:54 +01:00
c4f781e6e3
only process keyslots within a given range 2020-03-27 20:03:42 +01:00
f6de4a033e
more detailed messages 2020-03-27 18:28:33 +01:00
f5880346b9
switch to libcryptsetup-rs 2020-03-27 18:09:38 +01:00
6089b254b4
switch to libcryptsetup-rs for luks2 support 2020-03-22 17:39:44 +01:00
03e34ec790
0.2.3
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2020-01-20 22:43:06 +01:00
a437106fcb
use await-dev per default
All checks were successful
continuous-integration/drone/push Build is passing
2020-01-16 19:41:59 +01:00
7ed948d53b
update & tidy readme
All checks were successful
continuous-integration/drone/push Build is passing
2020-01-15 16:46:15 +01:00
c4e08413c0
Added --await-dev flag
All checks were successful
continuous-integration/drone/push Build is passing
2020-01-13 23:23:45 +01:00
shimunn
7429706920
Merge pull request #7 from mmahut/patch-1
All checks were successful
continuous-integration/drone/push Build is passing
error.rs: typo
2020-01-13 21:54:09 +01:00
Marek Mahut
a5fd5fa9f6
error.rs: typo 2020-01-13 17:44:51 +01:00
659fafdfb4
update to 0.2.2
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2020-01-10 21:44:33 +01:00
7f2668eded
allow for named credentials 2020-01-10 21:32:39 +01:00
shimunn
ae714cdef3
Merge pull request #6 from mmahut/fixid
All checks were successful
continuous-integration/drone/push Build is passing
match rp_id to fido2luks
2020-01-10 19:47:31 +01:00
shimunn
ae802e5e71
Merge pull request #5 from mmahut/env
use password helper in modified environments
2020-01-10 19:47:01 +01:00
Marek Mahut
a5f0444d24 match rp_id to fido2luks 2020-01-10 17:13:56 +01:00
Marek Mahut
a307d87d88 use password helper in modified environments 2020-01-10 16:52:22 +01:00
721dded6d2
WIP: 0.2.2
All checks were successful
continuous-integration/drone/push Build is passing
Warning: This release cointains changes to way credentials are generated,
which may cause your authenticator to reject the old credential.
2020-01-09 22:22:54 +01:00
e7049a281a
Use fido2luks as rp_id instead if default hmac, consider making
All checks were successful
continuous-integration/drone/push Build is passing
credenials device specific
2020-01-02 15:35:32 +01:00
5d1c7beb4d
added flag to retry open command
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-12 22:46:54 +02:00
2bac911b32
assigned exit codes to error cases 2019-10-12 22:46:20 +02:00
9a8ea993b5
fmt
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-12 13:40:24 +02:00
shimunn
7eb9dcc928
Merge pull request #2 from jannic/add-test-cases
Some checks failed
continuous-integration/drone/push Build is failing
Add test case for hash calculations
2019-10-12 13:24:33 +02:00
shimunn
509e300a8f
Merge pull request #1 from jannic/port-to-ring
Use ring for sha256 calculation
2019-10-12 13:24:18 +02:00
Jan Niehusmann
42945956a6 Add test case for hash calculations
While replacing the implementation of sha256, I noticed that there
is no test case actually calling the hash calculations.

Added two such test cases. Please note that I didn't verify that the
result is correct, but just took the value the existing implementation
returned. So those tests will only catch future regressions.
2019-10-11 22:15:21 +00:00
Jan Niehusmann
3cf5ccf2a0 Use ring for sha256 calculation
According to https://rustsec.org/advisories/RUSTSEC-2016-0005.html,
rust-crypto is unmaintained.

Crates depending on rust-crypto should be ported to other crates.

This port replaces rust-crypto with the sha2 implementation of ring,
as fido2luks already depends on it via ctap_hmac. Note that it uses
an old version of ring, so I used the same version, here.
2019-10-11 22:06:00 +00:00
79e9a37806
use cratesio deps only
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-10 13:41:02 +02:00
d16118e695
Readme
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-08 15:13:15 +02:00
6e53449ff6
move config into etc
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-08 14:50:31 +02:00
fbcfdea96b
make salt cli option 2019-10-06 22:16:12 +02:00
99e408cc8d
replaced InputSalt::Both with String option
All checks were successful
continuous-integration/drone/push Build is passing
2019-10-06 22:15:29 +02:00
8fc9e0dcce
extended readme
All checks were successful
continuous-integration/drone/push Build is passing
2019-09-27 01:03:33 +02:00
66 changed files with 3799 additions and 5553 deletions

View File

@ -3,32 +3,27 @@ name: default
steps:
- name: fmt
image: rust:1.37.0
image: rust:1.43.0
commands:
- rustup component add rustfmt
- cargo fmt --all -- --check
- rustup component add rustfmt
- cargo fmt --all -- --check
- name: test
image: rust:1.37.0
image: ubuntu:focal
environment:
DEBIAN_FRONTEND: noninteractive
commands:
- apt update && apt install -y libcryptsetup-dev libkeyutils-dev
- cargo test
- name: build
image: rust:1.37.0
- apt update && apt install -y cargo libkeyutils-dev libclang-dev clang pkg-config libcryptsetup-dev
- cargo test --locked
- name: publish
image: ubuntu:focal
environment:
DEBIAN_FRONTEND: noninteractive
CARGO_REGISTRY_TOKEN:
from_secret: cargo_tkn
commands:
- apt update && apt install -y libcryptsetup-dev libkeyutils-dev
- cargo install -f --path . --root .
- 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
when:
event: tag
- name: publish
image: plugins/github-release
settings:
api_key:
from_secret: github_release
files:
- bin/fido2luks
checksum:
- md5
- sha256
when:
event: tag

6
.gitignore vendored
View File

@ -2,3 +2,9 @@
**/*.rs.bk
.idea/
*.iml
fido2luks.bash
fido2luks.elv
fido2luks.fish
fido2luks.zsh
result
result-*

1095
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,38 @@
[package]
name = "fido2luks"
version = "0.2.1"
version = "0.2.20"
authors = ["shimunn <shimun@shimun.net>"]
edition = "2018"
description = "Decrypt your LUKS partition using a FIDO2 compatible authenticator"
license = "GPL v3"
documentation = "https://github.com/shimunn/fido2luks/blob/master/README.md"
homepage = "https://github.com/shimunn/fido2luks"
repository = "https://github.com/shimunn/fido2luks"
readme = "README.md"
keywords = ["luks", "fido2", "u2f"]
categories = ["command-line-utilities"]
license = "MPL-2.0"
[dependencies]
ctap = "0.1.0"
cryptsetup-rs = "0.2.0"
libcryptsetup-sys = "0.1.1"
ctap_hmac = { version="0.4.5", features = ["request_multiple"] }
hex = "0.3.2"
rust-crypto = "0.2.36"
ring = "0.13.5"
failure = "0.1.5"
rpassword = "4.0.1"
structopt = "0.3.2"
libcryptsetup-rs = "0.4.1"
serde_json = "1.0.51"
serde_derive = "1.0.106"
serde = "1.0.106"
[patch.crates-io]
#Until https://github.com/solidninja/cryptsetup-rs/pull/2 merges
cryptsetup-rs = { path = "./patch/cryptsetup-rs" }
libcryptsetup-sys = { path = "./patch/cryptsetup-rs/libcryptsetup-sys" }
ctap = { path = "./patch/ctap" }
[build-dependencies]
ctap_hmac = { version="0.4.5", features = ["request_multiple"] }
hex = "0.3.2"
ring = "0.13.5"
failure = "0.1.5"
rpassword = "4.0.1"
libcryptsetup-rs = "0.4.1"
structopt = "0.3.2"
[profile.release]
lto = true
@ -31,8 +41,16 @@ panic = 'abort'
incremental = false
overflow-checks = false
[package.metadata.rpm]
buildflags = ["--release"]
[package.metadata.rpm.targets]
fido2luks = { path = "/usr/bin/fido2luks" }
[package.metadata.deb]
depends = "$auto, cryptsetup"
build-depends = "libclang-dev, libcryptsetup-dev"
extended-description = "Decrypt your LUKS partition using a FIDO2 compatible authenticator"
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"],
]
conf-files = ["/etc/fido2luks.conf"]

1047
LICENSE

File diff suppressed because it is too large Load Diff

37
PKGBUILD Normal file
View File

@ -0,0 +1,37 @@
# Maintainer: shimunn <shimun@shimun.net>
pkgname=fido2luks-git
pkgver=0.2.16.7e6b33a
pkgrel=1
makedepends=('rust' 'cargo' 'cryptsetup' 'clang' 'git')
depends=('cryptsetup')
arch=('i686' 'x86_64' 'armv6h' 'armv7h')
pkgdesc="Decrypt your LUKS partition using a FIDO2 compatible authenticator"
url="https://github.com/shimunn/fido2luks"
license=('MPL-2.0')
source=('git+https://github.com/shimunn/fido2luks')
sha512sums=('SKIP')
pkgver() {
cd fido2luks
# Use tag version if possible otherwise concat project version and git ref
git describe --exact-match --tags HEAD 2>/dev/null ||
echo "$(cargo pkgid | cut -d'#' -f2).$(git describe --always)"
}
build() {
cd fido2luks
cargo build --release --locked --all-features --target-dir=target
}
package() {
cd fido2luks
install -Dm 755 target/release/fido2luks -t "${pkgdir}/usr/bin"
install -Dm 755 pam_mount/fido2luksmounthelper.sh -t "${pkgdir}/usr/bin"
install -Dm 644 initcpio/hooks/fido2luks -t "${pkgdir}/usr/lib/initcpio/hooks"
install -Dm 644 initcpio/install/fido2luks -t "${pkgdir}/usr/lib/initcpio/install"
install -Dm 644 fido2luks.bash "${pkgdir}/usr/share/bash-completion/completions/fido2luks"
install -Dm 644 fido2luks.fish -t "${pkgdir}/usr/share/fish/vendor_completions.d"
}

131
README.md
View File

@ -1,15 +1,15 @@
# fido2luks
# 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 compatable key
This will allow you to unlock your LUKS encrypted disk with an FIDO2 compatible key.
Note: This has only been tested under Fedora 30 using a Solo 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 cargo cryptsetup-devel -y
dnf install clang cargo cryptsetup-devel -y
```
### Device
@ -17,18 +17,24 @@ dnf install cargo cryptsetup-devel -y
```
git clone https://github.com/shimunn/fido2luks.git && cd fido2luks
#Alternativly cargo build --release && sudo cp target/release/fido2luks /usr/bin/
CARGO_INSTALL_ROOT=/usr sudo -E cargo install -f --path .
# Alternativly cargo build --release && sudo cp target/release/fido2luks /usr/bin/
sudo -E cargo install -f --path . --root /usr
echo FIDO2LUKS_CREDENTIAL_ID=$(fido2luks credential) >> fido2luks.conf
# 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
. fido2luks.conf
. /etc/fido2luks.conf
#Repeat for each luks volume
# 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)
# Test(only works if the luks container isn't active)
sudo -E fido2luks -i open /dev/disk/by-uuid/<DISK_UUID> luks-<DISK_UUID>
```
@ -43,21 +49,112 @@ sudo make install
### Grub
Add `rd.luks.2fa=<CREDENTIAL_ID>:<DISK_UUID>` to `GRUB_CMDLINE_LINUX`
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 addkey`
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
```
## Test
Just reboot and see if it works, if thats the case you should remove your old less secure password from your luks header:
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
```
#Recommend in case you lose your authenticator, store this backupfile somewhere safe
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
# 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.

24
build.rs Normal file
View File

@ -0,0 +1,24 @@
#![allow(warnings)]
#[macro_use]
extern crate failure;
extern crate ctap_hmac as ctap;
#[path = "src/cli_args/mod.rs"]
mod cli_args;
#[path = "src/error.rs"]
mod error;
#[path = "src/util.rs"]
mod util;
use cli_args::Args;
use std::env;
use std::str::FromStr;
use structopt::clap::Shell;
use structopt::StructOpt;
fn main() {
// 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(), ".");
}
}

View File

@ -0,0 +1,3 @@
FIDO2LUKS_SALT=Ask
FIDO2LUKS_PASSWORD_HELPER=/usr/bin/systemd-ask-password Please enter second factor for LUKS disk encryption

View File

@ -9,7 +9,7 @@ MOUNT=$(command -v mount)
UMOUNT=$(command -v umount)
TIMEOUT=120
CON_MSG="Please connect your authenicator"
CON_MSG="Please connect your authenticator"
generate_service () {
local credential_id=$1 target_uuid=$2 timeout=$3 sd_dir=${4:-$NORMAL_DIR}
@ -19,6 +19,10 @@ generate_service () {
local crypto_target_service="systemd-cryptsetup@luks\x2d${sd_target_uuid}.service"
local sd_service="${sd_dir}/luks-2fa@luks\x2d${sd_target_uuid}.service"
local fido2luks_args="--bin"
if [ ! -z "$timeout" ]; then
fido2luks_args="$fido2luks_args --await-dev ${timeout}"
fi
{
printf -- "[Unit]"
printf -- "\nDescription=%s" "2fa for luks"
@ -27,19 +31,15 @@ generate_service () {
printf -- "\nBefore=%s umount.target luks-2fa.target" "$crypto_target_service"
printf -- "\nConflicts=umount.target"
printf -- "\nDefaultDependencies=no"
printf -- "\nJobTimeoutSec=%s" "$timeout"
[ ! -z "$timeout" ] && printf -- "\nJobTimeoutSec=%s" "$timeout"
printf -- "\n\n[Service]"
printf -- "\nType=oneshot"
printf -- "\nRemainAfterExit=yes"
printf -- "\nEnvironment=FIDO2LUKS_CREDENTIAL_ID='%s'" "$credential_id"
printf -- "\nEnvironment=FIDO2LUKS_SALT='%s'" "Ask"
printf -- "\nEnvironment=FIDO2LUKS_PASSWORD_HELPER='%s'" "/usr/bin/systemd-ask-password Disk 2fa password"
printf -- "\nEnvironmentFile=%s" "/etc/fido2luks.conf"
[ ! -z "$credential_id" ] && printf -- "\nEnvironment=FIDO2LUKS_CREDENTIAL_ID='%s'" "$credential_id"
printf -- "\nKeyringMode=%s" "shared"
printf -- "\nExecStartPre=-/usr/bin/plymouth display-message --text \"${CON_MSG}\""
printf -- "\nExecStartPre=-/bin/bash -c \"while ! ${FIDO2LUKS} connected; do /usr/bin/sleep 1; done\""
printf -- "\nExecStartPre=-/usr/bin/plymouth hide-message --text \"${CON_MSG}\""
printf -- "\nExecStart=/bin/bash -c \"${FIDO2LUKS} print-secret --bin | ${CRYPTSETUP} attach 'luks-%s' '/dev/disk/by-uuid/%s' '/dev/stdin'\"" "$target_uuid" "$target_uuid"
printf -- "\nExecStartPre=-/usr/bin/plymouth display-message --text \"${CON_MSG}\""
printf -- "\nExecStart=/bin/bash -c \"${FIDO2LUKS} print-secret $fido2luks_args | ${CRYPTSETUP} attach 'luks-%s' '/dev/disk/by-uuid/%s' '/dev/stdin'\"" "$target_uuid" "$target_uuid"
printf -- "\nExecStop=${CRYPTSETUP} detach 'luks-%s'" "$target_uuid"
} > "$sd_service"

View File

@ -18,6 +18,7 @@ depends () {
install () {
inst "$moddir/luks-2fa-generator.sh" "/etc/systemd/system-generators/luks-2fa-generator.sh"
inst_simple "/usr/bin/fido2luks" "/usr/bin/fido2luks"
inst_simple "/etc/fido2luks.conf" "/etc/fido2luks.conf"
inst "$systemdutildir/systemd-cryptsetup"
mkdir -p "$initdir/luks-2fa"

View File

@ -15,6 +15,7 @@ help:
install:
cp ${MODULE_CONF_D}/${MODULE_CONF} ${DRACUT_CONF_D}/
cp -r ${MODULE_DIR} ${DRACUT_MODULES_D}/
cp ${MODULE_DIR}/fido2luks.conf /etc/fido2luks.conf
dracut -fv
clean:
rm ${DRACUT_CONF_D}/${MODULE_CONF}

62
flake.lock generated Normal file
View File

@ -0,0 +1,62 @@
{
"nodes": {
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1639947939,
"narHash": "sha256-pGsM8haJadVP80GFq4xhnSpNitYNQpaXk4cnA796Cso=",
"owner": "nmattia",
"repo": "naersk",
"rev": "2fc8ce9d3c025d59fee349c1f80be9785049d653",
"type": "github"
},
"original": {
"owner": "nmattia",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1638109994,
"narHash": "sha256-OpA37PTiPMIqoRJbufbl5rOLII7HeeGcA0yl7FoyCIE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a284564b7f75ac4db73607db02076e8da9d42c9d",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"utils": {
"locked": {
"lastModified": 1638122382,
"narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "74f7e4319258e287b0f9cb95426c9853b282730b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

61
flake.nix Normal file
View File

@ -0,0 +1,61 @@
{
description = "Decrypt your LUKS partition using a FIDO2 compatible authenticator";
inputs = {
utils.url = "github:numtide/flake-utils";
naersk = {
url = "github:nmattia/naersk";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, utils, naersk }:
let
root = ./.;
pname = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.name;
forPkgs = pkgs:
let
naersk-lib = naersk.lib."${pkgs.system}";
buildInputs = with pkgs; [ cryptsetup ];
LIBCLANG_PATH = "${pkgs.clang.cc.lib}/lib";
nativeBuildInputs = with pkgs; [
pkgconfig
clang
];
in
rec {
# `nix build`
packages.${pname} = naersk-lib.buildPackage {
inherit pname root buildInputs nativeBuildInputs LIBCLANG_PATH;
};
defaultPackage = packages.${pname};
# `nix run`
apps.${pname} = utils.lib.mkApp {
drv = packages.${pname};
};
defaultApp = apps.${pname};
# `nix flake check`
checks = {
fmt = with pkgs; runCommandLocal "${pname}-fmt" { buildInputs = [ cargo rustfmt nixpkgs-fmt ]; } ''
cd ${root}
cargo fmt -- --check
nixpkgs-fmt --check *.nix
touch $out
'';
};
# `nix develop`
devShell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ rustc cargo rustfmt nixpkgs-fmt ] ++ nativeBuildInputs;
inherit buildInputs LIBCLANG_PATH;
};
};
forSystem = system: forPkgs nixpkgs.legacyPackages."${system}";
in
(utils.lib.eachSystem [ "aarch64-linux" "i686-linux" "x86_64-linux" ] forSystem) // {
overlay = final: prev: (forPkgs final).packages;
};
}

18
initcpio/Makefile Normal file
View File

@ -0,0 +1,18 @@
.PHONY: install remove
install:
install -Dm644 hooks/fido2luks -t /usr/lib/initcpio/hooks
install -Dm644 install/fido2luks -t /usr/lib/initcpio/install
ifdef preset
mkinitcpio -p $(preset)
else
mkinitcpio -P
endif
remove:
rm /usr/lib/initcpio/{hooks,install}/fido2luks
ifdef preset
mkinitcpio -p $(preset)
else
mkinitcpio -P
endif

52
initcpio/README.md Normal file
View File

@ -0,0 +1,52 @@
## fido2luks hook for mkinitcpio (ArchLinux and derivatives)
> ⚠️ Before proceeding, it is very advised to [backup your existing LUKS2 header](https://wiki.archlinux.org/title/dm-crypt/Device_encryption#Backup_using_cryptsetup) to external storage
### Setup
1. Connect your FIDO2 authenticator
2. Generate credential id
```shell
fido2luks credential
```
3. Generate salt (random string)
```shell
pwgen 48 1
```
4. Add key to your LUKS2 device
```shell
fido2luks add-key -Pt --salt <salt> <block_device> <credential_id>
```
`-P` - request PIN to unlock the authenticator
`-t` - add token (including credential id) to the LUKS2 header
`-e` - wipe all other keys
For the full list of options see `fido2luks add-key --help`
5. Edit [/etc/fido2luks.conf](/initcpio/fido2luks.conf)
Keyslot (`FIDO2LUKS_DEVICE_SLOT`) can be obtained from the output of
```shell
cryptsetup luksDump <block_device>
```
6. Add fido2luks hook to /etc/mkinitcpio.conf
Before or instead of `encrypt` hook, for example:
```shell
HOOKS=(base udev autodetect modconf keyboard block fido2luks filesystems fsck)
```
7. Recreate initial ramdisk
```shell
mkinitcpio -p <preset>
```

18
initcpio/fido2luks.conf Normal file
View File

@ -0,0 +1,18 @@
# Set credential *ONLY IF* it's not embedded in the LUKS2 header
FIDO2LUKS_CREDENTIAL_ID=
# Encrypted device and its name under /dev/mapper
# Can be overridden by `cryptdevice` kernel parameter
FIDO2LUKS_DEVICE=
FIDO2LUKS_MAPPER_NAME=
FIDO2LUKS_SALT=string:<salt>
# Use specific keyslot (ignore all other slots)
FIDO2LUKS_DEVICE_SLOT=
# Await for an authenticator to be connected (in seconds)
FIDO2LUKS_DEVICE_AWAIT=
# Set to 1 if PIN is required to unlock the authenticator
FIDO2LUKS_ASK_PIN=

55
initcpio/hooks/fido2luks Normal file
View File

@ -0,0 +1,55 @@
#!/usr/bin/ash
run_hook() {
modprobe -a -q dm-crypt >/dev/null 2>&1
. /etc/fido2luks.conf
if [ -z "$cryptdevice" ]; then
device="$FIDO2LUKS_DEVICE"
dmname="$FIDO2LUKS_MAPPER_NAME"
else
IFS=: read cryptdev dmname _cryptoptions <<EOF
$cryptdevice
EOF
if ! device=$(resolve_device "${cryptdev}" ${rootdelay}); then
return 1
fi
fi
options="--salt $FIDO2LUKS_SALT"
if [ "$FIDO2LUKS_ASK_PIN" == 1 ]; then
options="$options --pin"
fi
if [ -n "$FIDO2LUKS_DEVICE_SLOT" ]; then
options="$options --slot $FIDO2LUKS_DEVICE_SLOT"
fi
if [ -n "$FIDO2LUKS_DEVICE_AWAIT" ]; then
options="$options --await-dev $FIDO2LUKS_DEVICE_AWAIT"
fi
# HACK: /dev/tty is hardcoded in rpassword, but not accessible from the ramdisk
# Temporary link it to /dev/tty1
mv /dev/tty /dev/tty.back
ln -s /dev/tty1 /dev/tty
printf "\nAuthentication is required to access the $dmname volume at $device\n"
if [ -z "$FIDO2LUKS_CREDENTIAL_ID" ]; then
fido2luks open-token $device $dmname $options
else
fido2luks open $device $dmname $FIDO2LUKS_CREDENTIAL_ID $options
fi
exit_code=$?
# Restore /dev/tty
mv /dev/tty.back /dev/tty
if [ $exit_code -ne 0 ]; then
printf '\n'
read -s -p 'Press Enter to continue'
printf '\n'
fi
}

View File

@ -0,0 +1,31 @@
#!/bin/bash
build() {
local mod
add_module dm-crypt
add_module dm-integrity
if [[ $CRYPTO_MODULES ]]; then
for mod in $CRYPTO_MODULES; do
add_module "$mod"
done
else
add_all_modules /crypto/
fi
add_binary fido2luks
add_binary dmsetup
add_file /usr/lib/udev/rules.d/10-dm.rules
add_file /usr/lib/udev/rules.d/13-dm-disk.rules
add_file /usr/lib/udev/rules.d/95-dm-notify.rules
add_file /usr/lib/initcpio/udev/11-dm-initramfs.rules /usr/lib/udev/rules.d/11-dm-initramfs.rules
add_file /etc/fido2luks.conf /etc/fido2luks.conf
add_runscript
}
help() {
cat <<HELPEOF
This hook allows to decrypt LUKS2 partition using FIDO2 compatible authenticator
HELPEOF
}

11
initramfs-tools/Makefile Normal file
View File

@ -0,0 +1,11 @@
.PHONY: install
install:
chmod +x hook/fido2luks.sh keyscript.sh
cp -f hook/fido2luks.sh /etc/initramfs-tools/hooks/
mkdir -p /usr/share/fido2luks
cp -f keyscript.sh /lib/cryptsetup/scripts/fido2luks
update-initramfs -u
remove:
sh -c "grep 'keyscript=fido2luks' -i /etc/crypttab && ( echo 'ERROR: your system is still setup to use fido2luks during boot' && exit 1) || exit 0"
rm /etc/initramfs-tools/hooks/fido2luks.sh /lib/cryptsetup/scripts/fido2luks
update-initramfs -u

34
initramfs-tools/README.md Normal file
View File

@ -0,0 +1,34 @@
## Initramfs-tools based systems(Ubuntu and derivatives)
For easiest installation [download and install the precompiled deb from releases.](https://github.com/shimunn/fido2luks/releases). However it is possible to build from source via the instructions on the main readme.
```
sudo -s
# Insert FIDO key.
fido2luks credential
# Tap FIDO key
# Copy returned string <CREDENTIAL>
nano /etc/fido2luks.conf
# Insert <CREDENTIAL>
# FIDO2LUKS_CREDENTIAL_ID=<CREDENTIAL>
set -a
. /etc/fido2luks.conf
fido2luks -i add-key /dev/<LUKS PARTITION>
# Current password: <Any current LUKS password>
# Password: <Password used as FIDO challange>
# Tap FIDO key
nano /etc/crypttab
# Append to end ",discard,initramfs,keyscript=fido2luks"
# E.g. sda6_crypt UUID=XXXXXXXXXX none luks,discard,initramfs,keyscript=fido2luks
update-initramfs -u
```
[Recording showing part of the setup](https://shimun.net/fido2luks/setup.svg)

View File

@ -0,0 +1,5 @@
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

@ -0,0 +1,14 @@
#!/bin/sh
case "$1" in
prereqs)
echo ""
exit 0
;;
esac
. /usr/share/initramfs-tools/hook-functions
copy_file config /etc/fido2luks.conf /etc/fido2luks.conf
copy_exec /usr/bin/fido2luks
exit 0

30
initramfs-tools/keyscript.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/sh
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

332
initramfs-tools/setup.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 550 KiB

220
pam_mount/fido2luksmounthelper.sh Executable file
View File

@ -0,0 +1,220 @@
#!/bin/bash
#
# This is a rather minimal example Argbash potential
# Example taken from http://argbash.readthedocs.io/en/stable/example.html
#
# ARG_POSITIONAL_SINGLE([operation],[Operation to perform (mount|umount)],[])
# ARG_OPTIONAL_SINGLE([credentials-type],[c],[Type of the credentials to use (external|embedded)])
# ARG_OPTIONAL_SINGLE([device],[d],[Name of the device to create])
# ARG_OPTIONAL_SINGLE([mount-point],[m],[Path of the mount point to use])
# ARG_OPTIONAL_BOOLEAN([ask-pin],[a],[Ask for a pin],[off])
# ARG_OPTIONAL_SINGLE([salt],[s],[Salt to use],[""])
# ARG_HELP([Unlocks/Locks a LUKS volume and mount/unmount it in the given mount point.])
# ARGBASH_GO()
# needed because of Argbash --> m4_ignore([
### START OF CODE GENERATED BY Argbash v2.9.0 one line above ###
# Argbash is a bash code generator used to get arguments parsing right.
# Argbash is FREE SOFTWARE, see https://argbash.io for more info
# Generated online by https://argbash.io/generate
die()
{
local _ret="${2:-1}"
test "${_PRINT_HELP:-no}" = yes && print_help >&2
echo "$1" >&2
exit "${_ret}"
}
begins_with_short_option()
{
local first_option all_short_options='cdmash'
first_option="${1:0:1}"
test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0
}
# THE DEFAULTS INITIALIZATION - POSITIONALS
_positionals=()
# THE DEFAULTS INITIALIZATION - OPTIONALS
_arg_credentials_type=
_arg_device=
_arg_mount_point=
_arg_ask_pin="off"
_arg_salt=""
print_help()
{
printf '%s\n' "Unlocks/Locks a LUKS volume and mount/unmount it in the given mount point."
printf 'Usage: %s [-c|--credentials-type <arg>] [-d|--device <arg>] [-m|--mount-point <arg>] [-a|--(no-)ask-pin] [-s|--salt <arg>] [-h|--help] <operation>\n' "$0"
printf '\t%s\n' "<operation>: Operation to perform (mount|umount)"
printf '\t%s\n' "-c, --credentials-type: Type of the credentials to use (external|embedded) (no default)"
printf '\t%s\n' "-d, --device: Name of the device to create (no default)"
printf '\t%s\n' "-m, --mount-point: Path of the mount point to use (no default)"
printf '\t%s\n' "-a, --ask-pin, --no-ask-pin: Ask for a pin (off by default)"
printf '\t%s\n' "-s, --salt: Salt to use (default: '""')"
printf '\t%s\n' "-h, --help: Prints help"
}
parse_commandline()
{
_positionals_count=0
while test $# -gt 0
do
_key="$1"
case "$_key" in
-c|--credentials-type)
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
_arg_credentials_type="$2"
shift
;;
--credentials-type=*)
_arg_credentials_type="${_key##--credentials-type=}"
;;
-c*)
_arg_credentials_type="${_key##-c}"
;;
-d|--device)
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
_arg_device="$2"
shift
;;
--device=*)
_arg_device="${_key##--device=}"
;;
-d*)
_arg_device="${_key##-d}"
;;
-m|--mount-point)
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
_arg_mount_point="$2"
shift
;;
--mount-point=*)
_arg_mount_point="${_key##--mount-point=}"
;;
-m*)
_arg_mount_point="${_key##-m}"
;;
-a|--no-ask-pin|--ask-pin)
_arg_ask_pin="on"
test "${1:0:5}" = "--no-" && _arg_ask_pin="off"
;;
-a*)
_arg_ask_pin="on"
_next="${_key##-a}"
if test -n "$_next" -a "$_next" != "$_key"
then
{ begins_with_short_option "$_next" && shift && set -- "-a" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option."
fi
;;
-s|--salt)
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
_arg_salt="$2"
shift
;;
--salt=*)
_arg_salt="${_key##--salt=}"
;;
-s*)
_arg_salt="${_key##-s}"
;;
-h|--help)
print_help
exit 0
;;
-h*)
print_help
exit 0
;;
*)
_last_positional="$1"
_positionals+=("$_last_positional")
_positionals_count=$((_positionals_count + 1))
;;
esac
shift
done
}
handle_passed_args_count()
{
local _required_args_string="'operation'"
test "${_positionals_count}" -ge 1 || _PRINT_HELP=yes die "FATAL ERROR: Not enough positional arguments - we require exactly 1 (namely: $_required_args_string), but got only ${_positionals_count}." 1
test "${_positionals_count}" -le 1 || _PRINT_HELP=yes die "FATAL ERROR: There were spurious positional arguments --- we expect exactly 1 (namely: $_required_args_string), but got ${_positionals_count} (the last one was: '${_last_positional}')." 1
}
assign_positional_args()
{
local _positional_name _shift_for=$1
_positional_names="_arg_operation "
shift "$_shift_for"
for _positional_name in ${_positional_names}
do
test $# -gt 0 || break
eval "$_positional_name=\${1}" || die "Error during argument parsing, possibly an Argbash bug." 1
shift
done
}
parse_commandline "$@"
handle_passed_args_count
assign_positional_args 1 "${_positionals[@]}"
# OTHER STUFF GENERATED BY Argbash
### END OF CODE GENERATED BY Argbash (sortof) ### ])
# [ <-- needed because of Argbash
if [ -z ${_arg_mount_point} ]; then
die "Missing '--mount-point' argument"
fi
if [ -z ${_arg_device} ]; then
die "Missing '--device' argument"
fi
ASK_PIN=${_arg_ask_pin}
OPERATION=${_arg_operation}
DEVICE=${_arg_device}
DEVICE_NAME=$(sed "s|/|_|g" <<< ${DEVICE})
MOUNT_POINT=${_arg_mount_point}
CREDENTIALS_TYPE=${_arg_credentials_type}
SALT=${_arg_salt}
CONF_FILE_PATH="/etc/fido2luksmounthelper.conf"
if [ "${OPERATION}" == "mount" ]; then
if [ "${CREDENTIALS_TYPE}" == "external" ]; then
if [ -f ${CONF_FILE_PATH} ]; then
if [ "${ASK_PIN}" == "on" ]; then
read PASSWORD
fi
CREDENTIALS=$(<${CONF_FILE_PATH})
else
die "The configuration file '${CONF_FILE_PATH}' is missing. Please create it or use embedded credentials."
fi
printf ${PASSWORD} | fido2luks open --salt string:${SALT} --pin --pin-source /dev/stdin ${DEVICE} ${DEVICE_NAME} ${CREDENTIALS}
elif [ "${CREDENTIALS_TYPE}" == "embedded" ]; then
if [ "${ASK_PIN}" == "on" ]; then
read PASSWORD
fi
printf ${PASSWORD} | fido2luks open-token --salt string:${SALT} --pin --pin-source /dev/stdin ${DEVICE} ${DEVICE_NAME}
else
die "Given credential-type '${CREDENTIALS_TYPE}' is invalid. It must be 'external' or 'embedded'"
fi
mount /dev/mapper/${DEVICE_NAME} ${MOUNT_POINT}
elif [ "${OPERATION}" == "umount" ]; then
umount ${MOUNT_POINT}
cryptsetup luksClose ${DEVICE_NAME}
else
die "Given operation '${OPERATION}' is invalid. It must be 'mount' or 'unmount'"
fi
exit 0
# ] <-- needed because of Argbash

View File

@ -1,4 +0,0 @@
target/
Cargo.lock
*.iml
.idea/

View File

@ -1,22 +0,0 @@
language: rust
rust:
- stable
- beta
- nightly
matrix:
allow_failures:
- rust: nightly
sudo: required
dist: trusty
os:
- linux
script:
- cargo build --all-targets --verbose
- sudo /bin/sh -c "PATH=/home/travis/.cargo/bin:$PATH LD_LIBRARY_PATH=/home/travis/.cargo/lib cargo test --verbose"
addons:
apt:
packages:
- libcryptsetup-dev
notifications:
email:
on_success: never

View File

@ -1,32 +0,0 @@
[package]
authors = ["Vladimir Lushnikov <vladimir@solidninja.is>"]
description = "Rust wrapper around the libcryptsetup library, allowing manipulation of LUKS devices in Linux"
homepage = "https://github.com/solidninja/cryptsetup-rs"
license = "LGPL-3.0"
name = "cryptsetup-rs"
version = "0.2.0"
[dependencies]
errno = "0.2.3"
libc = "0.2.42"
log = "0.4.2"
[dependencies.blkid-rs]
path = "blkid-rs"
version = "0.1.1"
[dependencies.libcryptsetup-sys]
path = "libcryptsetup-sys"
version = "0.1.1"
[dependencies.uuid]
features = ["v4"]
version = "0.6.5"
[dev-dependencies]
env_logger = "0.5.10"
expectest = "0.10.0"
tempdir = "0.3.7"
[lib]
name = "cryptsetup_rs"

View File

@ -1,165 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@ -1,29 +0,0 @@
[![Build Status](https://travis-ci.org/solidninja/cryptsetup-rs.svg?branch=master)](https://travis-ci.org/solidninja/cryptsetup-rs)
[![crates.io Status](https://img.shields.io/crates/v/cryptsetup-rs.svg)](https://crates.io/crates/cryptsetup-rs)
[![docs.rs build](https://docs.rs/cryptsetup-rs/badge.svg)](https://docs.rs/crate/cryptsetup-rs/)
# cryptsetup-rs - Rust bindings to `libcryptsetup` on Linux
A safe binding to `libcryptsetup` that allows working with encrypted disks on Linux.
Features:
* High-level API for open/format/other operations
Documentation for the bindings can be found on [docs.rs](https://docs.rs/crate/cryptsetup-rs/).
The example [`luks_dump.rs`](examples/luks_dump.rs) shows how a command like `cryptsetup luksDump` can
be implemented.
## TODO
* Secure string for passing keys
* High-level API for non-LUKS1 disks (truecrypt, verity)
* LUKS2 and cryptsetup2 support
## Contributing
`cryptsetup-rs` is the work of its contributors and is a free software project licensed under the
LGPLv3 or later.
If you would like to contribute, please follow the [C4](https://rfc.zeromq.org/spec:42/C4/) process.

View File

@ -1,2 +0,0 @@
target/
Cargo.lock

View File

@ -1,18 +0,0 @@
[package]
name = "blkid-rs"
version = "0.1.1"
authors = ["Vladimir Lushnikov <vladimir@solidninja.is>"]
description = "Rust implementation of blkid for LUKS volumes"
license = "LGPL-3.0"
homepage = "https://github.com/solidninja/libcryptset-rs"
[lib]
name = "blkid_rs"
[dependencies]
byteorder = "1.1"
uuid = "0.6"
[dev-dependencies]
env_logger = "0.4"
tempdir = "0.3"

View File

@ -1,286 +0,0 @@
// The following code has been ported from libcryptsetup
extern crate byteorder;
extern crate uuid;
use std::convert;
use std::io;
use std::io::Read;
use std::mem;
use std::str;
pub trait LuksHeader {
fn version(&self) -> u16;
fn cipher_name(&self) -> Result<&str, Error>;
fn cipher_mode(&self) -> Result<&str, Error>;
fn hash_spec(&self) -> Result<&str, Error>;
fn payload_offset(&self) -> u32;
fn key_bytes(&self) -> u32;
fn mk_digest(&self) -> &[u8];
fn mk_digest_salt(&self) -> &[u8];
fn mk_digest_iterations(&self) -> u32;
fn uuid(&self) -> Result<uuid::Uuid, Error>;
}
#[derive(Debug)]
pub enum Error {
InvalidMagic,
InvalidStringEncoding(str::Utf8Error),
InvalidVersion,
InvalidUuid(uuid::ParseError),
ReadError(io::Error),
ReadIncorrectHeaderSize,
HeaderProcessingError,
}
pub struct BlockDevice;
impl BlockDevice {
pub fn read_luks_header<R: Read>(reader: R) -> Result<raw::luks_phdr, Error> {
let luks_phdr_size = mem::size_of::<raw::luks_phdr>();
let mut buf = Vec::<u8>::with_capacity(luks_phdr_size);
let read_size = try!(reader.take(luks_phdr_size as u64).read_to_end(&mut buf));
if read_size != luks_phdr_size {
Err(Error::ReadIncorrectHeaderSize)
} else {
raw::luks_phdr::from_buf(&buf)
}
}
}
impl convert::From<str::Utf8Error> for Error {
fn from(error: str::Utf8Error) -> Error {
Error::InvalidStringEncoding(error)
}
}
impl convert::From<uuid::ParseError> for Error {
fn from(error: uuid::ParseError) -> Error {
Error::InvalidUuid(error)
}
}
impl convert::From<io::Error> for Error {
fn from(error: io::Error) -> Error {
Error::ReadError(error)
}
}
/* FIXME
impl convert::From<byteorder::Error> for Error {
fn from(error: byteorder::Error) -> Error {
match error {
byteorder::Error::UnexpectedEOF => Error::HeaderProcessingError,
byteorder::Error::Io(io_error) => Error::ReadError(io_error),
}
}
}
*/
pub mod raw {
#![allow(non_snake_case)]
use std::convert::From;
use std::io::{Cursor, Read};
use std::mem;
use std::str;
use byteorder::{BigEndian, ReadBytesExt};
use uuid;
const LUKS_VERSION_SUPPORTED: u16 = 1;
const LUKS_MAGIC_L: usize = 6;
const LUKS_CIPHERNAME_L: usize = 32;
const LUKS_CIPHERMODE_L: usize = 32;
const LUKS_HASHSPEC_L: usize = 32;
const LUKS_DIGESTSIZE: usize = 20;
const LUKS_SALTSIZE: usize = 32;
const UUID_STRING_L: usize = 40;
const LUKS_MAGIC: &'static [u8; LUKS_MAGIC_L] = b"LUKS\xba\xbe";
#[repr(C, packed)]
pub struct luks_phdr {
pub magic: [u8; LUKS_MAGIC_L],
pub version: u16,
pub cipherName: [u8; LUKS_CIPHERNAME_L],
pub cipherMode: [u8; LUKS_CIPHERMODE_L],
pub hashSpec: [u8; LUKS_HASHSPEC_L],
pub payloadOffset: u32,
pub keyBytes: u32,
pub mkDigest: [u8; LUKS_DIGESTSIZE],
pub mkDigestSalt: [u8; LUKS_SALTSIZE],
pub mkDigestIterations: u32,
pub uuid: [u8; UUID_STRING_L],
}
impl luks_phdr {
pub fn from_buf(buf: &[u8]) -> Result<luks_phdr, super::Error> {
// FIXME - this is not particularly pretty
if buf.len() != mem::size_of::<luks_phdr>() {
return Err(super::Error::ReadIncorrectHeaderSize);
}
let mut cursor = Cursor::new(buf);
let mut magic_buf = [0u8; LUKS_MAGIC_L];
let magic_len = try!(cursor.read(&mut magic_buf));
if magic_len != LUKS_MAGIC_L || magic_buf != &LUKS_MAGIC[..] {
return Err(super::Error::InvalidMagic);
}
let version = try!(cursor.read_u16::<BigEndian>());
if version != LUKS_VERSION_SUPPORTED {
return Err(super::Error::InvalidVersion);
}
let mut cipher_name_buf = [0u8; LUKS_CIPHERNAME_L];
let cipher_name_len = try!(cursor.read(&mut cipher_name_buf));
if cipher_name_len != LUKS_CIPHERNAME_L {
return Err(super::Error::HeaderProcessingError);
}
let mut cipher_mode_buf = [0u8; LUKS_CIPHERMODE_L];
let cipher_mode_len = try!(cursor.read(&mut cipher_mode_buf));
if cipher_mode_len != LUKS_CIPHERMODE_L {
return Err(super::Error::HeaderProcessingError);
}
let mut hash_spec_buf = [0u8; LUKS_HASHSPEC_L];
let hash_spec_len = try!(cursor.read(&mut hash_spec_buf));
if hash_spec_len != LUKS_HASHSPEC_L {
return Err(super::Error::HeaderProcessingError);
}
let payload_offset = try!(cursor.read_u32::<BigEndian>());
let key_bytes = try!(cursor.read_u32::<BigEndian>());
let mut mk_digest_buf = [0u8; LUKS_DIGESTSIZE];
let mk_digest_len = try!(cursor.read(&mut mk_digest_buf));
if mk_digest_len != LUKS_DIGESTSIZE {
return Err(super::Error::HeaderProcessingError);
}
let mut mk_digest_salt_buf = [0u8; LUKS_SALTSIZE];
let mk_digest_salt_len = try!(cursor.read(&mut mk_digest_salt_buf));
if mk_digest_salt_len != LUKS_SALTSIZE {
return Err(super::Error::HeaderProcessingError);
}
let mk_digest_iterations = try!(cursor.read_u32::<BigEndian>());
let mut uuid_buf = [0u8; UUID_STRING_L];
let uuid_len = try!(cursor.read(&mut uuid_buf));
if uuid_len != UUID_STRING_L {
return Err(super::Error::HeaderProcessingError);
}
let res = luks_phdr {
magic: magic_buf,
version: version,
cipherName: cipher_name_buf,
cipherMode: cipher_mode_buf,
hashSpec: hash_spec_buf,
payloadOffset: payload_offset,
keyBytes: key_bytes,
mkDigest: mk_digest_buf,
mkDigestSalt: mk_digest_salt_buf,
mkDigestIterations: mk_digest_iterations,
uuid: uuid_buf,
};
Ok(res)
}
}
fn u8_buf_to_str(buf: &[u8]) -> Result<&str, super::Error> {
if let Some(pos) = buf.iter().position(|&c| c == 0) {
str::from_utf8(&buf[0..pos]).map_err(From::from)
} else {
str::from_utf8(buf).map_err(From::from)
}
}
impl super::LuksHeader for luks_phdr {
fn version(&self) -> u16 {
self.version
}
fn cipher_name(&self) -> Result<&str, super::Error> {
u8_buf_to_str(&self.cipherName)
}
fn cipher_mode(&self) -> Result<&str, super::Error> {
u8_buf_to_str(&self.cipherMode)
}
fn hash_spec(&self) -> Result<&str, super::Error> {
u8_buf_to_str(&self.hashSpec)
}
fn payload_offset(&self) -> u32 {
self.payloadOffset
}
fn key_bytes(&self) -> u32 {
self.keyBytes
}
fn mk_digest(&self) -> &[u8] {
&self.mkDigest
}
fn mk_digest_salt(&self) -> &[u8] {
&self.mkDigestSalt
}
fn mk_digest_iterations(&self) -> u32 {
self.mkDigestIterations
}
fn uuid(&self) -> Result<uuid::Uuid, super::Error> {
let uuid_str = try!(u8_buf_to_str(&self.uuid));
uuid::Uuid::parse_str(uuid_str).map_err(From::from)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use uuid;
#[test]
fn test_luks_header_from_byte_buffer() {
let header = b"LUKS\xba\xbe\x00\x01aes\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ecb\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00sha256\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00 \xcf^\xb4\xc00q\xbe\xd5\xe6\x90\xc8G\xb3\x00\xbe\xba\xd052qp\x92\x0c\x9c\xa9\x07R\\y_D\x08b\xf1\xe6\x8f\x0c\xa95\xad\xdb\x15+\xa5\xd7\xa7\xbf^\x96B\x90z\x00\x00\x03\xe8a1b49d2d-8a7e-4b04-ab2a-89f3408fd198\x00\x00\x00\x00";
let mut cursor: Cursor<&[u8]> = Cursor::new(header);
let luks_header = BlockDevice::read_luks_header(&mut cursor).unwrap();
assert_eq!(luks_header.version(), 1);
assert_eq!(luks_header.cipher_name().unwrap(), "aes");
assert_eq!(luks_header.cipher_mode().unwrap(), "ecb");
assert_eq!(luks_header.hash_spec().unwrap(), "sha256");
assert_eq!(luks_header.payload_offset(), 4096);
assert_eq!(luks_header.key_bytes(), 32);
assert_eq!(
luks_header.mk_digest(),
&[
207, 94, 180, 192, 48, 113, 190, 213, 230, 144, 200, 71, 179, 0, 190, 186, 208, 53,
50, 113
]
);
assert_eq!(
luks_header.mk_digest_salt(),
&[
112, 146, 12, 156, 169, 7, 82, 92, 121, 95, 68, 8, 98, 241, 230, 143, 12, 169, 53,
173, 219, 21, 43, 165, 215, 167, 191, 94, 150, 66, 144, 122
]
);
assert_eq!(luks_header.mk_digest_iterations(), 1000);
assert_eq!(
luks_header.uuid().unwrap(),
uuid::Uuid::parse_str("a1b49d2d-8a7e-4b04-ab2a-89f3408fd198").unwrap()
)
}
}

View File

@ -1,78 +0,0 @@
#![deny(warnings)]
#[warn(unused_must_use)]
extern crate cryptsetup_rs;
extern crate env_logger;
use cryptsetup_rs::*;
use std::env;
fn dump_slot(crypt_device: &Luks1CryptDevice, slot: Keyslot) -> Result<()> {
let status = match crypt_device.keyslot_status(slot) {
crypt_keyslot_info::CRYPT_SLOT_INVALID => "INVALID",
crypt_keyslot_info::CRYPT_SLOT_INACTIVE => "DISABLED",
crypt_keyslot_info::CRYPT_SLOT_ACTIVE | crypt_keyslot_info::CRYPT_SLOT_ACTIVE_LAST => "ENABLED",
};
println!("Key Slot {}: {}", slot, status);
match status {
"ENABLED" => /* TODO add keyslot information */ (),
_ => (),
}
Ok(())
}
fn dump(device_path: &str) -> Result<()> {
let dev = open(device_path)?.luks1()?;
println!("LUKS header information for {}", dev.device_name());
println!();
println!("{:<16}{}", "Version:", "1");
println!("{:<16}{}", "Cipher name:", dev.cipher());
println!("{:<16}{}", "Cipher mode:", dev.cipher_mode());
println!("{:<16}{}", "Hash spec:", dev.hash_spec());
println!("{:<16}{}", "Payload offset:", dev.payload_offset());
println!("{:<16}{}", "MK bits:", dev.mk_bits());
print!("{:<16}", "MK digest:");
for b in dev.mk_digest().iter() {
print!("{:x} ", b);
}
println!();
let salt = dev.mk_salt();
print!("{:<16}", "MK salt:");
for b in salt[0..16].iter() {
print!("{:x} ", b);
}
println!();
print!("{:<16}", "");
for b in salt[16..32].iter() {
print!("{:x} ", b);
}
println!();
println!("{:<16}{}", "MK iterations:", dev.mk_iterations());
println!("{:<16}{}", "UUID:", dev.uuid());
println!();
for slot in 0..8 {
dump_slot(&dev, slot)?;
}
Ok(())
}
fn main() {
let args: Vec<String> = env::args().skip(1).collect();
if args.len() != 1 {
println!("Usage: luks_dump <device path>");
::std::process::exit(1);
}
let device_path = args[0].as_str();
if let Err(e) = dump(device_path) {
println!("Error: {:?}", e);
::std::process::exit(2);
}
}

View File

@ -1,21 +0,0 @@
[package]
name = "libcryptsetup-sys"
version = "0.1.1"
authors = ["Vladimir Lushnikov <vladimir@solidninja.is>"]
license = "LGPL-3.0"
description = "FFI bindings to the libcryptsetup library"
homepage = "https://github.com/solidninja/cryptsetup-rs"
links = "cryptsetup"
build = "build.rs"
[lib]
name = "libcryptsetup_sys"
path = "lib.rs"
[dependencies]
libc = "0.2"
[build-dependencies]
pkg-config = "0.3"

View File

@ -1,8 +0,0 @@
extern crate pkg_config;
fn main() {
pkg_config::Config::new()
.statik(true)
.find("libcryptsetup")
.unwrap();
}

View File

@ -1,492 +0,0 @@
#![deny(warnings)]
#![allow(non_camel_case_types)]
extern crate libc;
use libc::{c_char, c_double, c_int, c_uint, c_void, size_t};
use std::str::FromStr;
pub enum crypt_device {}
#[repr(i32)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum crypt_log_level {
CRYPT_LOG_NORMAL = 0,
CRYPT_LOG_ERROR = 1,
CRYPT_LOG_VERBOSE = 2,
CRYPT_LOG_DEBUG = -1,
}
pub type crypt_log_cb = extern "C" fn(crypt_log_level, *const c_char, *mut c_void);
pub type crypt_confirm_cb = extern "C" fn(*const c_char, *mut c_void) -> c_int;
pub type crypt_password_cb =
extern "C" fn(*const c_char, *mut c_char, size_t, *mut c_void) -> c_int;
#[repr(i32)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum crypt_rng_type {
CRYPT_RNG_URANDOM = 0,
CRYPT_RNG_RANDOM = 1,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum crypt_device_type {
PLAIN,
LUKS1,
LOOPAES,
VERITY,
TCRYPT,
}
#[repr(C)]
pub struct crypt_params_plain {
pub hash: *const c_char,
pub offset: u64,
pub skip: u64,
pub size: u64,
}
#[repr(C)]
pub struct crypt_params_luks1 {
pub hash: *const c_char,
pub data_alignment: size_t,
pub data_device: *const c_char,
}
#[repr(C)]
pub struct crypt_params_loopaes {
pub hash: *const c_char,
pub offset: u64,
pub skip: u64,
}
#[repr(C)]
pub struct crypt_params_verity {
pub hash_name: *const c_char,
pub data_device: *const c_char,
pub hash_device: *const c_char,
pub salt: *const c_char,
pub salt_size: u32,
pub hash_type: u32,
pub data_block_size: u32,
pub hash_block_size: u32,
pub data_size: u64,
pub hash_area_offset: u64,
pub flags: u32,
}
#[repr(u32)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum crypt_verity_flag {
CRYPT_VERITY_NO_HEADER = (1 << 0),
CRYPT_VERITY_CHECK_HASH = (1 << 1),
CRYPT_VERITY_CREATE_HASH = (1 << 2),
}
#[repr(C)]
pub struct crypt_params_tcrypt {
pub passphrase: *const c_char,
pub passphrase_size: size_t,
pub keyfiles: *const *const c_char,
pub keyfiles_count: c_uint,
pub hash_name: *const c_char,
pub cipher: *const c_char,
pub mode: *const c_char,
pub key_size: size_t,
pub flags: u32,
}
#[repr(u32)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum crypt_tcrypt_flag {
CRYPT_TCRYPT_LEGACY_MODES = (1 << 0),
CRYPT_TCRYPT_HIDDEN_HEADER = (1 << 1),
CRYPT_TCRYPT_BACKUP_HEADER = (1 << 2),
CRYPT_TCRYPT_SYSTEM_HEADER = (1 << 3),
CRYPT_TCRYPT_VERA_MODES = (1 << 4),
}
pub const CRYPT_ANY_SLOT: c_int = -1;
#[repr(u32)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum crypt_activation_flag {
CRYPT_ACTIVATE_READONLY = (1 << 0),
CRYPT_ACTIVATE_NO_UUID = (1 << 1),
CRYPT_ACTIVATE_SHARED = (1 << 2),
CRYPT_ACTIVATE_ALLOW_DISCARDS = (1 << 3),
CRYPT_ACTIVATE_PRIVATE = (1 << 4),
CRYPT_ACTIVATE_CORRUPTED = (1 << 5),
CRYPT_ACTIVATE_SAME_CPU_CRYPT = (1 << 6),
CRYPT_ACTIVATE_SUBMIT_FROM_CRYPT_CPUS = (1 << 7),
CRYPT_ACTIVATE_IGNORE_CORRUPTION = (1 << 8),
CRYPT_ACTIVATE_RESTART_ON_CORRUPTION = (1 << 9),
CRYPT_ACTIVATE_IGNORE_ZERO_BLOCKS = (1 << 10),
}
#[repr(C)]
pub struct crypt_active_device {
pub offset: u64,
pub iv_offset: u64,
pub size: u64,
pub flags: u32,
}
#[repr(C)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum crypt_status_info {
CRYPT_INVALID,
CRYPT_INACTIVE,
CRYPT_ACTIVE,
CRYPT_BUSY,
}
#[repr(C)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum crypt_keyslot_info {
CRYPT_SLOT_INVALID,
CRYPT_SLOT_INACTIVE,
CRYPT_SLOT_ACTIVE,
CRYPT_SLOT_ACTIVE_LAST,
}
#[repr(C)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum crypt_debug_level {
CRYPT_DEBUG_ALL = -1,
CRYPT_DEBUG_NONE = 0,
}
extern "C" {
pub fn crypt_init(cd: *mut *mut crypt_device, device: *const c_char) -> c_int;
pub fn crypt_init_by_name_and_header(
cd: *mut *mut crypt_device,
name: *const c_char,
header_device: *const c_char,
) -> c_int;
pub fn crypt_init_by_name(cd: *mut *mut crypt_device, name: *const c_char) -> c_int;
pub fn crypt_set_log_callback(
cd: *mut crypt_device,
log: Option<crypt_log_cb>,
usrptr: *mut c_void,
);
pub fn crypt_log(cd: *mut crypt_device, level: crypt_log_level, msg: *const c_char);
pub fn crypt_set_confirm_callback(
cd: *mut crypt_device,
confirm: crypt_confirm_cb,
usrptr: *mut c_void,
);
#[deprecated]
pub fn crypt_set_password_callback(
cd: *mut crypt_device,
password: crypt_password_cb,
usrptr: *mut c_void,
);
#[deprecated]
pub fn crypt_set_timeout(cd: *mut crypt_device, timeout: u64);
#[deprecated]
pub fn crypt_set_password_retry(cd: *mut crypt_device, tries: c_int);
pub fn crypt_set_iteration_time(cd: *mut crypt_device, iteration_time_ms: u64);
#[deprecated]
pub fn crypt_set_password_verify(cd: *mut crypt_device, password_verify: c_int);
pub fn crypt_set_data_device(cd: *mut crypt_device, device: *const c_char) -> c_int;
pub fn crypt_set_rng_type(cd: *mut crypt_device, rng_type: crypt_rng_type);
pub fn crypt_get_rng_type(cd: *mut crypt_device) -> c_int;
pub fn crypt_memory_lock(cd: *mut crypt_device, lock: c_int) -> c_int;
pub fn crypt_get_type(cd: *mut crypt_device) -> *const c_char;
pub fn crypt_format(
cd: *mut crypt_device,
crypt_type: *const c_char,
cipher: *const c_char,
cipher_mode: *const c_char,
uuid: *const c_char,
volume_key: *const c_char,
volume_key_size: size_t,
params: *mut c_void,
) -> c_int;
pub fn crypt_set_uuid(cd: *mut crypt_device, uuid: *const c_char) -> c_int;
pub fn crypt_load(
cd: *mut crypt_device,
requested_type: *const c_char,
params: *mut c_void,
) -> c_int;
pub fn crypt_repair(
cd: *mut crypt_device,
requested_type: *const c_char,
params: *mut c_void,
) -> c_int;
pub fn crypt_resize(cd: *mut crypt_device, name: *const c_char, new_size: u64) -> c_int;
pub fn crypt_suspend(cd: *mut crypt_device, name: *const c_char) -> c_int;
pub fn crypt_resume_by_passphrase(
cd: *mut crypt_device,
name: *const c_char,
keyslot: c_int,
passphrase: *const c_char,
passphrase_size: size_t,
) -> c_int;
pub fn crypt_resume_by_keyfile_offset(
cd: *mut crypt_device,
name: *const c_char,
keyslot: c_int,
keyfile: *const c_char,
keyfile_size: size_t,
keyfile_offset: size_t,
) -> c_int;
pub fn crypt_resume_by_keyfile(
cd: *mut crypt_device,
name: *const c_char,
keyslot: c_int,
keyfile: *const c_char,
keyfile_size: size_t,
) -> c_int;
pub fn crypt_free(cd: *mut crypt_device);
pub fn crypt_keyslot_add_by_passphrase(
cd: *mut crypt_device,
keyslot: c_int,
passphrase: *const c_char,
passphrase_size: size_t,
new_passphrase: *const c_char,
new_passphrase_size: size_t,
) -> c_int;
pub fn crypt_keyslot_change_by_passphrase(
cd: *mut crypt_device,
keyslot_old: c_int,
keyslot_new: c_int,
passphrase: *const c_char,
passphrase_size: size_t,
new_passphrase: *const c_char,
new_passphrase_size: size_t,
) -> c_int;
pub fn crypt_keyslot_add_by_keyfile_offset(
cd: *mut crypt_device,
keyslot: c_int,
keyfile: *const c_char,
keyfile_size: size_t,
keyfile_offset: size_t,
new_keyfile: *const c_char,
new_keyfile_size: size_t,
new_keyfile_offset: size_t,
) -> c_int;
pub fn crypt_keyslot_add_by_keyfile(
cd: *mut crypt_device,
keyslot: c_int,
keyfile: *const c_char,
keyfile_size: size_t,
new_keyfile: *const c_char,
new_keyfile_size: size_t,
) -> c_int;
pub fn crypt_keyslot_add_by_volume_key(
cd: *mut crypt_device,
keyslot: c_int,
volume_key: *const c_char,
volume_key_size: size_t,
passphrase: *const c_char,
passphrase_size: size_t,
) -> c_int;
pub fn crypt_keyslot_destroy(cd: *mut crypt_device, keyslot: c_int) -> c_int;
pub fn crypt_get_active_device(
cd: *mut crypt_device,
name: *const c_char,
cad: *mut crypt_active_device,
) -> c_int;
pub fn crypt_activate_by_passphrase(
cd: *mut crypt_device,
name: *const c_char,
keyslot: c_int,
passphrase: *const c_char,
passphrase_size: size_t,
flags: u32,
) -> c_int;
pub fn crypt_activate_by_keyfile_offset(
cd: *mut crypt_device,
name: *const c_char,
keyslot: c_int,
keyfile: *const c_char,
keyfile_size: size_t,
keyfile_offset: size_t,
flags: u32,
) -> c_int;
pub fn crypt_activate_by_keyfile(
cd: *mut crypt_device,
name: *const c_char,
keyslot: c_int,
keyfile: *const c_char,
keyfile_size: size_t,
flags: u32,
) -> c_int;
pub fn crypt_activate_by_volume_key(
cd: *mut crypt_device,
name: *const c_char,
volume_key: *const c_char,
volume_key_size: size_t,
flags: u32,
) -> c_int;
pub fn crypt_deactivate(cd: *mut crypt_device, name: *const c_char) -> c_int;
pub fn crypt_volume_key_get(
cd: *mut crypt_device,
keyslot: c_int,
volume_key: *mut c_char,
volume_key_size: *mut size_t,
passphrase: *const c_char,
passphrase_size: size_t,
) -> c_int;
pub fn crypt_volume_key_verify(
cd: *mut crypt_device,
volume_key: *const c_char,
volume_key_size: size_t,
) -> c_int;
pub fn crypt_status(cd: *mut crypt_device, name: *const c_char) -> crypt_status_info;
pub fn crypt_dump(cd: *mut crypt_device) -> c_int;
pub fn crypt_get_cipher(cd: *mut crypt_device) -> *const c_char;
pub fn crypt_get_cipher_mode(cd: *mut crypt_device) -> *const c_char;
pub fn crypt_get_uuid(cd: *mut crypt_device) -> *const c_char;
pub fn crypt_get_device_name(cd: *mut crypt_device) -> *const c_char;
pub fn crypt_get_data_offset(cd: *mut crypt_device) -> u64;
pub fn crypt_get_iv_offset(cd: *mut crypt_device) -> u64;
pub fn crypt_get_volume_key_size(cd: *mut crypt_device) -> c_int;
pub fn crypt_get_verity_info(cd: *mut crypt_device, vp: *mut crypt_params_verity);
pub fn crypt_benchmark(
cd: *mut crypt_device,
cipher: *const c_char,
cipher_mode: *const c_char,
volume_key_size: size_t,
iv_size: size_t,
buffer_size: size_t,
encryption_mbs: *mut c_double,
decryption_mbs: *mut c_double,
) -> c_int;
pub fn crypt_benchmark_kdf(
cd: *mut crypt_device,
kdf: *const c_char,
hash: *const c_char,
password: *const c_char,
password_size: size_t,
salt: *const c_char,
salt_size: size_t,
iterations_sec: *mut u64,
) -> c_int;
pub fn crypt_keyslot_status(cd: *mut crypt_device, keyslot: c_int) -> crypt_keyslot_info;
pub fn crypt_keyslot_max(crypt_device_type: *const c_char) -> c_int;
pub fn crypt_keyslot_area(
cd: *mut crypt_device,
keyslot: c_int,
offset: *mut u64,
length: *mut u64,
) -> c_int;
pub fn crypt_header_backup(
cd: *mut crypt_device,
requested_type: *const c_char,
backup_file: *const c_char,
) -> c_int;
pub fn crypt_header_restore(
cd: *mut crypt_device,
requested_type: *const c_char,
backup_file: *const c_char,
) -> c_int;
#[deprecated]
pub fn crypt_last_error(cd: *mut crypt_device, buf: *mut c_char, size: size_t);
#[deprecated]
pub fn crypt_get_error(buf: *mut c_char, size: size_t);
pub fn crypt_get_dir() -> *const c_char;
pub fn crypt_set_debug_level(level: crypt_debug_level);
}
impl FromStr for crypt_device_type {
type Err = ();
fn from_str(s: &str) -> Result<crypt_device_type, ()> {
match s {
"PLAIN" => Ok(crypt_device_type::PLAIN),
"LUKS1" => Ok(crypt_device_type::LUKS1),
"LOOPAES" => Ok(crypt_device_type::LOOPAES),
"VERITY" => Ok(crypt_device_type::VERITY),
"TCRYPT" => Ok(crypt_device_type::TCRYPT),
_ => Err(()),
}
}
}
impl crypt_device_type {
pub fn to_str(&self) -> &str {
match self {
&crypt_device_type::PLAIN => "PLAIN",
&crypt_device_type::LUKS1 => "LUKS1",
&crypt_device_type::LOOPAES => "LOOPAES",
&crypt_device_type::VERITY => "VERITY",
&crypt_device_type::TCRYPT => "TCRYPT",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::str::FromStr;
#[test]
fn test_device_type_conversion() {
assert_eq!(
Ok(crypt_device_type::PLAIN),
crypt_device_type::from_str("PLAIN")
);
assert_eq!(
Ok(crypt_device_type::LUKS1),
crypt_device_type::from_str("LUKS1")
);
assert_eq!(
Ok(crypt_device_type::LOOPAES),
crypt_device_type::from_str("LOOPAES")
);
assert_eq!(
Ok(crypt_device_type::VERITY),
crypt_device_type::from_str("VERITY")
);
assert_eq!(
Ok(crypt_device_type::TCRYPT),
crypt_device_type::from_str("TCRYPT")
);
}
#[test]
fn test_keyslot_max_gt_zero() {
unsafe {
let luks_type = CString::new("LUKS1").unwrap();
assert!(crypt_keyslot_max(luks_type.as_ptr()) > 0);
}
}
}

View File

@ -1,2 +0,0 @@
max_width = 120
format_strings = false

View File

@ -1,377 +0,0 @@
//! High-level API to work with `libcryptsetup` supported devices (disks)
use std::fmt;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::ptr;
use blkid_rs::{BlockDevice, LuksHeader};
use device;
pub use device::enable_debug;
use device::RawDevice;
pub use device::{Error, Keyslot, Result};
use raw;
use uuid;
pub type Luks1CryptDeviceHandle = CryptDeviceHandle<Luks1Params>;
/// Builder to open a crypt device at the specified path
///
/// # Examples
///
/// ```
/// use cryptsetup_rs::*;
/// # fn foo() -> Result<()> {
/// let device = open("/dev/loop0")?.luks1()?;
/// # Ok(())
/// # }
/// ```
pub fn open<P: AsRef<Path>>(path: P) -> Result<CryptDeviceOpenBuilder> {
let cd = device::init(path.as_ref())?;
Ok(CryptDeviceOpenBuilder {
path: path.as_ref().to_owned(),
cd,
})
}
/// Builder to format a crypt device at the specified path
///
/// # Examples
///
/// ```
/// # extern crate uuid;
/// # extern crate cryptsetup_rs;
/// use cryptsetup_rs::*;
/// use uuid::Uuid;
///
/// # fn foo() -> Result<()> {
/// let uuid = Uuid::new_v4();
/// let device = format("/dev/loop0")?
/// .rng_type(crypt_rng_type::CRYPT_RNG_URANDOM)
/// .iteration_time(5000)
/// .luks1("aes", "xts-plain", "sha256", 256, Some(&uuid))?;
/// # Ok(())
/// # }
/// ```
pub fn format<P: AsRef<Path>>(path: P) -> Result<CryptDeviceFormatBuilder> {
let cd = device::init(path.as_ref())?;
Ok(CryptDeviceFormatBuilder {
path: path.as_ref().to_owned(),
cd,
})
}
/// Read the UUID of a LUKS1 container without opening the device
pub fn luks1_uuid<P: AsRef<Path>>(path: P) -> Result<uuid::Uuid> {
let device_file = File::open(path.as_ref())?;
let luks_phdr = BlockDevice::read_luks_header(device_file)?;
let uuid = luks_phdr.uuid()?;
Ok(uuid)
}
fn load_luks1_params<P: AsRef<Path>>(path: P) -> Result<Luks1Params> {
let device_file = File::open(path.as_ref())?;
let luks_phdr = BlockDevice::read_luks_header(device_file)?;
Luks1Params::from(luks_phdr)
}
/// Struct containing state for the `open()` builder
pub struct CryptDeviceOpenBuilder {
path: PathBuf,
cd: RawDevice,
}
impl CryptDeviceOpenBuilder {
/// Loads an existing LUKS1 crypt device
pub fn luks1(self: CryptDeviceOpenBuilder) -> Result<CryptDeviceHandle<Luks1Params>> {
let _ = device::load(&self.cd, raw::crypt_device_type::LUKS1);
let params = load_luks1_params(&self.path)?;
Ok(CryptDeviceHandle {
cd: self.cd,
path: self.path,
params,
})
}
}
/// Struct containing state for the `format()` builder
pub struct CryptDeviceFormatBuilder {
path: PathBuf,
cd: RawDevice,
}
impl CryptDeviceFormatBuilder {
/// Set the iteration time for the `PBKDF2` function. Note that this does not affect the MK iterations.
pub fn iteration_time(mut self, iteration_time_ms: u64) -> Self {
device::set_iteration_time(&mut self.cd, iteration_time_ms);
self
}
/// Set the random number generator to use
pub fn rng_type(mut self, rng_type: raw::crypt_rng_type) -> Self {
device::set_rng_type(&mut self.cd, rng_type);
self
}
/// Formats a new block device as a LUKS1 crypt device with the specified parameters
pub fn luks1(
mut self: CryptDeviceFormatBuilder,
cipher: &str,
cipher_mode: &str,
hash: &str,
mk_bits: usize,
maybe_uuid: Option<&uuid::Uuid>,
) -> Result<CryptDeviceHandle<Luks1Params>> {
let _ = device::luks1_format(&mut self.cd, cipher, cipher_mode, hash, mk_bits, maybe_uuid)?;
let params = load_luks1_params(&self.path)?;
Ok(CryptDeviceHandle {
cd: self.cd,
path: self.path,
params,
})
}
}
/// Trait representing common operations on a crypt device
pub trait CryptDevice {
/// Path the device was opened/created with
fn path(&self) -> &Path;
/// Name of cipher used
fn cipher(&self) -> &str;
/// Name of cipher mode used
fn cipher_mode(&self) -> &str;
/// Path to the underlying device (as reported by `libcryptsetup`)
fn device_name(&self) -> &str;
/// Random number generator used for operations on this crypt device
fn rng_type(&self) -> raw::crypt_rng_type;
/// Sets the random number generator to use
fn set_rng_type(&mut self, rng_type: raw::crypt_rng_type);
/// Sets the iteration time for the `PBKDF2` function. Note that this does not affect the MK iterations.
fn set_iteration_time(&mut self, iteration_time_ms: u64);
/// Volume key size (in bytes)
fn volume_key_size(&self) -> u8;
}
/// Trait for querying the device type at runtime
pub trait CryptDeviceType {
/// Type of the crypt device
fn device_type(&self) -> raw::crypt_device_type;
}
/// Trait representing specific operations on a LUKS1 device
pub trait Luks1CryptDevice {
/// Activate the crypt device, and give it the specified name
fn activate(&mut self, name: &str, key: &[u8]) -> Result<Keyslot>;
/// Add a new keyslot with the specified key
fn add_keyslot(
&mut self,
key: &[u8],
maybe_prev_key: Option<&[u8]>,
maybe_keyslot: Option<Keyslot>,
) -> Result<Keyslot>;
/// Replace an old key with a new one
fn update_keyslot(&mut self, key: &[u8], prev_key: &[u8], maybe_keyslot: Option<Keyslot>) -> Result<Keyslot>;
/// Destroy (and disable) key slot
fn destroy_keyslot(&mut self, slot: Keyslot) -> Result<()>;
/// Dump text-formatted information about the current device to stdout
fn dump(&self);
/// Get the hash algorithm used
fn hash_spec(&self) -> &str;
/// Get status of key slot
fn keyslot_status(&self, keyslot: Keyslot) -> raw::crypt_keyslot_info;
/// Number of bits in the master key
fn mk_bits(&self) -> u32;
/// Master key header digest
fn mk_digest(&self) -> &[u8; 20];
/// Master key `PBKDF2` iterations
fn mk_iterations(&self) -> u32;
/// Master key salt
fn mk_salt(&self) -> &[u8; 32];
/// Get the offset of the payload
fn payload_offset(&self) -> u32;
/// UUID of the current device
fn uuid(&self) -> uuid::Uuid;
}
/// An opaque handle on an initialized crypt device
#[derive(PartialEq)]
pub struct CryptDeviceHandle<P: fmt::Debug> {
/// Pointer to the raw device
cd: RawDevice,
/// Path to the crypt device (useful for diagnostics)
path: PathBuf,
/// Additional parameters depending on type of crypt device opened
params: P,
}
impl<P: fmt::Debug> fmt::Debug for CryptDeviceHandle<P> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"CryptDeviceHandle(path={}, raw={:p}, params={:?})",
self.path.display(),
self.cd,
self.params
)
}
}
impl<P: fmt::Debug> Drop for CryptDeviceHandle<P> {
fn drop(&mut self) {
device::free(&mut self.cd);
self.cd = ptr::null_mut();
}
}
impl<P: fmt::Debug> CryptDevice for CryptDeviceHandle<P> {
fn path(&self) -> &Path {
self.path.as_ref()
}
fn cipher(&self) -> &str {
device::cipher(&self.cd).expect("Initialised device should have cipher")
}
fn cipher_mode(&self) -> &str {
device::cipher_mode(&self.cd).expect("Initialised device should have cipher mode")
}
fn device_name(&self) -> &str {
device::device_name(&self.cd).expect("Initialised device should have an underlying path")
}
fn rng_type(&self) -> raw::crypt_rng_type {
device::rng_type(&self.cd)
}
fn set_rng_type(&mut self, rng_type: raw::crypt_rng_type) {
device::set_rng_type(&mut self.cd, rng_type)
}
fn set_iteration_time(&mut self, iteration_time_ms: u64) {
device::set_iteration_time(&mut self.cd, iteration_time_ms)
}
fn volume_key_size(&self) -> u8 {
device::volume_key_size(&self.cd)
}
}
/// Struct for storing LUKS1 parameters in memory
#[derive(Debug, PartialEq)]
pub struct Luks1Params {
hash_spec: String,
payload_offset: u32,
mk_bits: u32,
mk_digest: [u8; 20],
mk_salt: [u8; 32],
mk_iterations: u32,
}
impl Luks1Params {
fn from(header: impl LuksHeader) -> Result<Luks1Params> {
let hash_spec = header.hash_spec()?.to_owned();
let payload_offset = header.payload_offset();
let mk_bits = header.key_bytes() * 8;
let mut mk_digest = [0u8; 20];
mk_digest.copy_from_slice(header.mk_digest());
let mut mk_salt = [0u8; 32];
mk_salt.copy_from_slice(header.mk_digest_salt());
let mk_iterations = header.mk_digest_iterations();
Ok(Luks1Params {
hash_spec,
payload_offset,
mk_bits,
mk_digest,
mk_salt,
mk_iterations,
})
}
}
impl Luks1CryptDevice for CryptDeviceHandle<Luks1Params> {
fn activate(&mut self, name: &str, key: &[u8]) -> Result<Keyslot> {
device::luks_activate(&mut self.cd, name, key)
}
fn add_keyslot(
&mut self,
key: &[u8],
maybe_prev_key: Option<&[u8]>,
maybe_keyslot: Option<Keyslot>,
) -> Result<Keyslot> {
device::luks_add_keyslot(&mut self.cd, key, maybe_prev_key, maybe_keyslot)
}
fn update_keyslot(&mut self, key: &[u8], prev_key: &[u8], maybe_keyslot: Option<Keyslot>) -> Result<Keyslot> {
device::luks_update_keyslot(&mut self.cd, key, prev_key, maybe_keyslot)
}
fn destroy_keyslot(&mut self, slot: Keyslot) -> Result<()> {
device::luks_destroy_keyslot(&mut self.cd, slot)
}
fn dump(&self) {
device::dump(&self.cd).expect("Dump should be fine for initialised device")
}
fn hash_spec(&self) -> &str {
self.params.hash_spec.as_ref()
}
fn keyslot_status(&self, keyslot: Keyslot) -> raw::crypt_keyslot_info {
device::keyslot_status(&self.cd, keyslot)
}
fn mk_bits(&self) -> u32 {
self.params.mk_bits
}
fn mk_digest(&self) -> &[u8; 20] {
&self.params.mk_digest
}
fn mk_iterations(&self) -> u32 {
self.params.mk_iterations
}
fn mk_salt(&self) -> &[u8; 32] {
&self.params.mk_salt
}
fn payload_offset(&self) -> u32 {
self.params.payload_offset
}
fn uuid(&self) -> uuid::Uuid {
device::uuid(&self.cd).expect("LUKS1 device should have UUID")
}
}
impl CryptDeviceType for CryptDeviceHandle<Luks1Params> {
fn device_type(&self) -> raw::crypt_device_type {
raw::crypt_device_type::LUKS1
}
}

View File

@ -1,319 +0,0 @@
//! Low-level cryptsetup binding that sits directly on top of the `libcryptsetup` C API
//!
//! Consider using the high-level binding in the `api` module instead
use std::ffi;
use std::mem;
use std::path::Path;
use std::ptr;
use std::result;
use std::str;
use blkid_rs;
use errno;
use libc;
use raw;
use uuid;
/// Raw pointer to the underlying `crypt_device` opaque struct
pub type RawDevice = *mut raw::crypt_device;
#[derive(Debug)]
pub enum Error {
/// Error that originates from `libcryptsetup` (with numeric error code)
CryptsetupError(errno::Errno),
/// IO error
IOError(::std::io::Error),
/// Error from the blkid-rs library (while reading LUKS1 header)
BlkidError(blkid_rs::Error),
}
impl From<::std::io::Error> for Error {
fn from(e: ::std::io::Error) -> Self {
Error::IOError(e)
}
}
impl From<blkid_rs::Error> for Error {
fn from(e: blkid_rs::Error) -> Self {
Error::BlkidError(e)
}
}
pub type Result<T> = result::Result<T, Error>;
pub type Keyslot = u8;
const ANY_KEYSLOT: libc::c_int = -1 as libc::c_int;
fn str_from_c_str<'a>(c_str: *const libc::c_char) -> Option<&'a str> {
if c_str.is_null() {
None
} else {
unsafe { Some(ffi::CStr::from_ptr(c_str).to_str().unwrap()) }
}
}
macro_rules! crypt_error {
($res:expr) => {
Err(Error::CryptsetupError(errno::Errno(-$res)))
};
}
macro_rules! check_crypt_error {
($res:expr) => {
if $res != 0 {
crypt_error!($res)
} else {
Ok(())
}
};
}
/// Log function callback used by `libcryptsetup`
#[allow(unused)]
#[no_mangle]
pub extern "C" fn cryptsetup_rs_log_callback(
level: raw::crypt_log_level,
message: *const libc::c_char,
usrptr: *mut libc::c_void,
) {
let msg = str_from_c_str(message).unwrap();
match level {
raw::crypt_log_level::CRYPT_LOG_NORMAL => info!("{}", msg.trim_right()),
raw::crypt_log_level::CRYPT_LOG_ERROR => error!("{}", msg.trim_right()),
raw::crypt_log_level::CRYPT_LOG_VERBOSE => debug!("{}", msg.trim_right()),
raw::crypt_log_level::CRYPT_LOG_DEBUG => debug!("{}", msg.trim_right()),
}
}
/// Enable internal `libcryptsetup` debugging
pub fn enable_debug(debug: bool) {
if debug {
unsafe { raw::crypt_set_debug_level(raw::crypt_debug_level::CRYPT_DEBUG_ALL) };
} else {
unsafe { raw::crypt_set_debug_level(raw::crypt_debug_level::CRYPT_DEBUG_NONE) };
}
}
/// Initialise crypt device and check if provided device exists
pub fn init<P: AsRef<Path>>(path: P) -> Result<RawDevice> {
let mut cd = ptr::null_mut();
let c_path = ffi::CString::new(path.as_ref().to_str().unwrap()).unwrap();
let res = unsafe { raw::crypt_init(&mut cd as *mut *mut raw::crypt_device, c_path.as_ptr()) };
if res != 0 {
crypt_error!(res)
} else {
unsafe {
raw::crypt_set_log_callback(cd, Some(cryptsetup_rs_log_callback), ptr::null_mut());
}
Ok(cd)
}
}
/// Load crypt device parameters from the on-disk header
///
/// Note that typically you cannot query the crypt device for information before this function is
/// called.
pub fn load(cd: &RawDevice, requested_type: raw::crypt_device_type) -> Result<()> {
let c_type = ffi::CString::new(requested_type.to_str()).unwrap();
let res = unsafe { raw::crypt_load(*cd, c_type.as_ptr(), ptr::null_mut()) };
check_crypt_error!(res)
}
/// Get the cipher used by this crypt device
pub fn cipher<'a>(cd: &'a RawDevice) -> Option<&'a str> {
let c_cipher = unsafe { raw::crypt_get_cipher(*cd) };
str_from_c_str(c_cipher)
}
/// Get the cipher mode used by this crypt device
pub fn cipher_mode<'a>(cd: &'a RawDevice) -> Option<&'a str> {
let c_cipher_mode = unsafe { raw::crypt_get_cipher_mode(*cd) };
str_from_c_str(c_cipher_mode)
}
/// Get the path to the device (as `libcryptsetup` sees it)
pub fn device_name<'a>(cd: &'a RawDevice) -> Option<&'a str> {
let c_device_name = unsafe { raw::crypt_get_device_name(*cd) };
str_from_c_str(c_device_name)
}
/// Dump text-formatted information about this device to the console
pub fn dump(cd: &RawDevice) -> Result<()> {
let res = unsafe { raw::crypt_dump(*cd) };
check_crypt_error!(res)
}
/// Releases crypt device context and memory
pub fn free(cd: &mut RawDevice) {
unsafe { raw::crypt_free(*cd) }
}
/// Activate device based on provided key ("passphrase")
pub fn luks_activate(cd: &mut RawDevice, name: &str, key: &[u8]) -> Result<Keyslot> {
let c_name = ffi::CString::new(name).unwrap();
let c_passphrase_len = key.len() as libc::size_t;
// cast the passphrase to a pointer directly - it will not be NUL terminated but the passed length is used
let c_passphrase = key as *const [u8] as *const libc::c_char;
let res = unsafe {
raw::crypt_activate_by_passphrase(*cd, c_name.as_ptr(), ANY_KEYSLOT, c_passphrase, c_passphrase_len, 0u32)
};
if res < 0 {
crypt_error!(res)
} else {
Ok(res as u8)
}
}
/// Add key slot using provided passphrase. If there is no previous passphrase, use the volume key
/// that is in-memory to add the new key slot.
pub fn luks_add_keyslot(
cd: &mut RawDevice,
key: &[u8],
maybe_prev_key: Option<&[u8]>,
maybe_keyslot: Option<Keyslot>,
) -> Result<Keyslot> {
let c_key_len = key.len() as libc::size_t;
let c_key = key as *const [u8] as *const libc::c_char;;
let c_keyslot = maybe_keyslot
.map(|k| k as libc::c_int)
.unwrap_or(ANY_KEYSLOT as libc::c_int);
let res = if let Some(prev_key) = maybe_prev_key {
let c_prev_key_len = prev_key.len() as libc::size_t;
let c_prev_key = prev_key as *const [u8] as *const libc::c_char;;
unsafe { raw::crypt_keyslot_add_by_passphrase(*cd, c_keyslot, c_prev_key, c_prev_key_len, c_key, c_key_len) }
} else {
unsafe {
raw::crypt_keyslot_add_by_volume_key(*cd, c_keyslot, ptr::null(), 0 as libc::size_t, c_key, c_key_len)
}
};
if res < 0 {
crypt_error!(res)
} else {
Ok(res as Keyslot)
}
}
/// Add key slot using provided passphrase. If there is no previous passphrase, use the volume key
/// that is in-memory to add the new key slot.
pub fn luks_update_keyslot(
cd: &mut RawDevice,
key: &[u8],
prev_key: &[u8],
maybe_keyslot: Option<Keyslot>,
) -> Result<Keyslot> {
let c_key_len = key.len() as libc::size_t;
let c_key = key as *const [u8] as *const libc::c_char;;
let c_keyslot = maybe_keyslot
.map(|k| k as libc::c_int)
.unwrap_or(ANY_KEYSLOT as libc::c_int);
let c_prev_key_len = prev_key.len() as libc::size_t;
let c_prev_key = prev_key as *const [u8] as *const libc::c_char;;
let res = unsafe {
raw::crypt_keyslot_change_by_passphrase(*cd, c_keyslot, c_keyslot, c_prev_key, c_prev_key_len, c_key, c_key_len)
};
if res < 0 {
crypt_error!(res)
} else {
Ok(res as Keyslot)
}
}
/// Destroy (and disable) key slot
pub fn luks_destroy_keyslot(cd: &mut RawDevice, keyslot: Keyslot) -> Result<()> {
let res = unsafe { raw::crypt_keyslot_destroy(*cd, keyslot as libc::c_int) };
if res < 0 {
crypt_error!(res)
} else {
Ok(())
}
}
/// Format a new crypt device but do not activate it
///
/// Note this does not add an active keyslot
pub fn luks1_format(
cd: &mut RawDevice,
cipher: &str,
cipher_mode: &str,
hash: &str,
mk_bits: usize,
maybe_uuid: Option<&uuid::Uuid>,
) -> Result<()> {
let c_cipher = ffi::CString::new(cipher).unwrap();
let c_cipher_mode = ffi::CString::new(cipher_mode).unwrap();
let c_hash = ffi::CString::new(hash).unwrap();
let c_uuid = maybe_uuid.map(|uuid| ffi::CString::new(uuid.hyphenated().to_string()).unwrap());
let mut luks_params = raw::crypt_params_luks1 {
hash: c_hash.as_ptr(),
data_alignment: 0,
data_device: ptr::null(),
};
let c_luks_params: *mut raw::crypt_params_luks1 = &mut luks_params;
let c_luks_type = ffi::CString::new(raw::crypt_device_type::LUKS1.to_str()).unwrap();
let c_uuid_ptr = c_uuid.as_ref().map(|u| u.as_ptr()).unwrap_or(ptr::null());
let res = unsafe {
raw::crypt_format(
*cd,
c_luks_type.as_ptr(),
c_cipher.as_ptr(),
c_cipher_mode.as_ptr(),
c_uuid_ptr,
ptr::null(),
mk_bits / 8,
c_luks_params as *mut libc::c_void,
)
};
check_crypt_error!(res)
}
/// Get which RNG is used
pub fn rng_type(cd: &RawDevice) -> raw::crypt_rng_type {
unsafe {
let res = raw::crypt_get_rng_type(*cd);
mem::transmute(res)
}
}
/// Set the number of milliseconds for `PBKDF2` function iteration
pub fn set_iteration_time(cd: &mut RawDevice, iteration_time_ms: u64) {
unsafe {
raw::crypt_set_iteration_time(*cd, iteration_time_ms);
}
}
/// Set which RNG is used
pub fn set_rng_type(cd: &mut RawDevice, rng_type: raw::crypt_rng_type) {
unsafe { raw::crypt_set_rng_type(*cd, rng_type) }
}
/// Get information about a keyslot
pub fn keyslot_status(cd: &RawDevice, slot: Keyslot) -> raw::crypt_keyslot_info {
unsafe { raw::crypt_keyslot_status(*cd, slot as libc::c_int) }
}
/// Get size in bytes of the volume key
pub fn volume_key_size(cd: &RawDevice) -> u8 {
let res = unsafe { raw::crypt_get_volume_key_size(*cd) };
res as u8
}
/// Get device UUID
pub fn uuid<'a>(cd: &'a RawDevice) -> Option<uuid::Uuid> {
let c_uuid_str = unsafe { raw::crypt_get_uuid(*cd) };
str_from_c_str(c_uuid_str).and_then(|uuid_str| uuid::Uuid::parse_str(uuid_str).ok())
}

View File

@ -1,32 +0,0 @@
//! Rust bindings to `libcryptsetup` - working with encrypted disks on Linux
//!
//! # Example
//!
//! See `api` module documentation for more.
//!
//! ```
//! use cryptsetup_rs::*;
//! # fn foo() -> Result<()> {
//! let device = open("/dev/loop0")?.luks1()?;
//! println!("Device UUID: {}", device.uuid());
//! println!("Device cipher: {}", device.cipher());
//! # Ok(())
//! # }
//! ```
#[warn(unused_must_use)]
extern crate blkid_rs;
extern crate errno;
extern crate libc;
extern crate libcryptsetup_sys as raw;
extern crate uuid;
#[macro_use]
extern crate log;
pub mod api;
pub mod device;
pub use api::{enable_debug, format, luks1_uuid, open};
pub use api::{CryptDevice, CryptDeviceType, Error, Keyslot, Luks1CryptDevice, Luks1CryptDeviceHandle, Result};
pub use raw::{crypt_device_type, crypt_keyslot_info, crypt_rng_type};

View File

@ -1,72 +0,0 @@
#![deny(warnings)]
extern crate cryptsetup_rs;
extern crate env_logger;
extern crate log;
extern crate tempdir;
extern crate uuid;
#[macro_use]
extern crate expectest;
use std::process::Command;
use expectest::prelude::*;
use tempdir::TempDir;
use uuid::Uuid;
use cryptsetup_rs::*;
struct TestContext {
dir: TempDir,
name: String,
}
impl TestContext {
fn new(name: String) -> TestContext {
env_logger::init();
cryptsetup_rs::enable_debug(true);
let dir = tempdir::TempDir::new(&name).unwrap();
TestContext { name, dir }
}
fn new_crypt_device(&self) -> api::CryptDeviceFormatBuilder {
let crypt_file = self.dir.path().join(format!("{}.image", self.name));
let dd_status = Command::new("dd")
.arg("if=/dev/zero")
.arg(format!("of={}", crypt_file.display()))
.arg("bs=1M")
.arg("count=10")
.status()
.unwrap();
if !dd_status.success() {
panic!("Failed to create disk image at {}", crypt_file.display());
}
cryptsetup_rs::format(crypt_file).unwrap()
}
}
#[test]
fn test_create_new_luks1_cryptdevice_no_errors() {
let ctx = TestContext::new("new_luks1_cryptdevice".to_string());
let uuid = Uuid::new_v4();
let device_format = ctx.new_crypt_device()
.rng_type(crypt_rng_type::CRYPT_RNG_URANDOM)
.iteration_time(42);
let mut dev = device_format
.luks1("aes", "xts-plain", "sha256", 256, Some(&uuid))
.expect("LUKS format should succeed");
dev.dump();
expect!(dev.uuid()).to(be_equal_to(uuid));
expect!(dev.device_type()).to(be_equal_to(crypt_device_type::LUKS1));
expect!(dev.cipher()).to(be_equal_to("aes"));
expect!(dev.cipher_mode()).to(be_equal_to("xts-plain"));
expect!(dev.volume_key_size()).to(be_equal_to(32));
expect!(dev.add_keyslot(b"hello world", None, Some(3))).to(be_ok().value(3));
}

View File

@ -1,4 +0,0 @@
/target
**/*.rs.bk
Cargo.lock
src/bin

View File

@ -1,21 +0,0 @@
[package]
name = "ctap"
description = "A Rust implementation of the FIDO2 CTAP protocol"
version = "0.1.0"
license = "Apache-2.0/MIT"
homepage = "https://github.com/ArdaXi/ctap"
repository = "https://github.com/ArdaXi/ctap"
authors = ["Arda Xi <arda@ardaxi.com>"]
edition = "2018"
[dependencies]
rand = "0.6"
failure = "0.1"
failure_derive = "0.1"
num-traits = "0.2"
num-derive = "0.2"
byteorder = "1"
cbor-codec = "0.7"
ring = "0.13"
untrusted = "0.6"
rust-crypto = "0.2"

View File

@ -1,59 +0,0 @@
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# ctap
ctap is a library implementing the [FIDO2 CTAP](https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html) protocol.
## Usage example
```rust
let devices = ctap::get_devices()?;
let device_info = &devices[0];
let mut device = ctap::FidoDevice::new(device_info)?;
// This can be omitted if the FIDO device is not configured with a PIN.
let pin = "test";
device.unlock(pin)?;
// In a real application these values would come from the requesting app.
let rp_id = "rp_id";
let user_id = [0];
let user_name = "user_name";
let client_data_hash = [0; 32];
let cred = device.make_credential(
rp_id,
&user_id,
user_name,
&client_data_hash
)?;
// In a real application the credential would be stored and used later.
let result = device.get_assertion(&cred, &client_data_hash);
```
## Limitations
Currently, this library only supports Linux. Testing and contributions for
other platforms is welcome.
## License
Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally
submitted for inclusion in the work by you, as defined in the Apache-2.0
license, shall be dual licensed as above, without any additional terms or
conditions.

View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,19 +0,0 @@
Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,715 +0,0 @@
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use cbor_codec::value;
use cbor_codec::value::Value;
use cbor_codec::{Config, Decoder, Encoder, GenericDecoder, GenericEncoder};
use byteorder::{BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt};
use failure::ResultExt;
use std::collections::HashMap;
use std::io::Cursor;
use super::error::*;
pub enum Request<'a> {
MakeCredential(MakeCredentialRequest<'a>),
GetAssertion(GetAssertionRequest<'a>),
GetInfo,
ClientPin(ClientPinRequest<'a>),
}
impl<'a> Request<'a> {
pub fn encode<W: WriteBytesExt>(&self, writer: &mut W) -> FidoResult<()> {
let mut encoder = Encoder::new(writer);
match self {
Request::MakeCredential(req) => req.encode(&mut encoder),
Request::GetAssertion(req) => req.encode(&mut encoder),
Request::GetInfo => encoder
.writer()
.write_u8(0x04)
.context(FidoErrorKind::CborEncode)
.map_err(From::from),
Request::ClientPin(req) => req.encode(&mut encoder),
}
}
pub fn decode<R: ReadBytesExt>(&self, reader: R) -> FidoResult<Response> {
Ok(match self {
Request::MakeCredential(_) => {
Response::MakeCredential(MakeCredentialResponse::decode(reader)?)
}
Request::GetAssertion(_) => {
Response::GetAssertion(GetAssertionResponse::decode(reader)?)
}
Request::GetInfo => Response::GetInfo(GetInfoResponse::decode(reader)?),
Request::ClientPin(_) => Response::ClientPin(ClientPinResponse::decode(reader)?),
})
}
}
#[derive(Debug)]
pub enum Response {
MakeCredential(MakeCredentialResponse),
GetAssertion(GetAssertionResponse),
GetInfo(GetInfoResponse),
ClientPin(ClientPinResponse),
}
#[derive(Default, Debug)]
pub struct MakeCredentialRequest<'a> {
pub client_data_hash: &'a [u8],
pub rp: PublicKeyCredentialRpEntity<'a>,
pub user: PublicKeyCredentialUserEntity<'a>,
pub pub_key_cred_params: &'a [(&'a str, i32)],
pub exclude_list: &'a [PublicKeyCredentialDescriptor],
pub extensions: &'a [(&'a str, &'a Value)],
pub options: Option<AuthenticatorOptions>,
pub pin_auth: Option<[u8; 16]>,
pub pin_protocol: Option<u8>,
}
impl<'a> MakeCredentialRequest<'a> {
pub fn encode<W: WriteBytesExt>(&self, mut encoder: &mut Encoder<W>) -> FidoResult<()> {
encoder
.writer()
.write_u8(0x01)
.context(FidoErrorKind::CborEncode)?; // authenticatorMakeCredential
let mut length = 4;
length += !self.exclude_list.is_empty() as usize;
length += !self.extensions.is_empty() as usize;
length += self.options.is_some() as usize;
length += self.pin_auth.is_some() as usize;
length += self.pin_protocol.is_some() as usize;
encoder.object(length)?;
encoder.u8(0x01)?; // clientDataHash
encoder.bytes(&self.client_data_hash)?;
encoder.u8(0x02)?; // rp
self.rp.encode(&mut encoder)?;
encoder.u8(0x03)?; // user
self.user.encode(&mut encoder)?;
encoder.u8(0x04)?; // pubKeyCredParams
encoder.array(self.pub_key_cred_params.len())?;
for (cred_type, alg) in self.pub_key_cred_params {
encoder.object(2)?;
encoder.text("alg")?;
encoder.i32(*alg)?;
encoder.text("type")?;
encoder.text(&cred_type)?;
}
if self.exclude_list.len() > 0 {
encoder.u8(0x05)?; // excludeList
encoder.array(self.exclude_list.len())?;
for item in self.exclude_list {
item.encode(&mut encoder)?;
}
}
if self.extensions.len() > 0 {
encoder.u8(0x06)?; // extensions
encoder.object(self.extensions.len())?;
for (key, value) in self.extensions {
encoder.text(key)?;
let mut generic = GenericEncoder::new(encoder.writer());
generic.value(value)?;
}
}
if let Some(options) = &self.options {
if options.encoded() {
encoder.u8(0x07)?; // options
options.encode(&mut encoder)?;
}
}
if let Some(pin_auth) = &self.pin_auth {
encoder.u8(0x08)?; // pinAuth
encoder.bytes(pin_auth)?;
}
if let Some(pin_protocol) = &self.pin_protocol {
encoder.u8(0x09)?; // pinProtocol
encoder.u8(*pin_protocol)?;
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct MakeCredentialResponse {
pub format: String,
pub auth_data: AuthenticatorData,
}
impl MakeCredentialResponse {
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
if status != 0 {
Err(FidoErrorKind::CborError(status))?
}
let mut decoder = Decoder::new(Config::default(), reader);
let mut response = MakeCredentialResponse::default();
for _ in 0..decoder.object()? {
let key = decoder.u8()?;
match key {
0x01 => response.format = decoder.text()?,
0x02 => response.auth_data = AuthenticatorData::from_bytes(&decoder.bytes()?)?,
0x03 => break, // TODO: parse attestation
_ => continue,
}
}
Ok(response)
}
}
#[derive(Debug, Default)]
pub struct GetAssertionRequest<'a> {
pub rp_id: &'a str,
pub client_data_hash: &'a [u8],
pub allow_list: &'a [PublicKeyCredentialDescriptor],
pub extensions: &'a [(&'a str, &'a Value)],
pub options: Option<AuthenticatorOptions>,
pub pin_auth: Option<[u8; 16]>,
pub pin_protocol: Option<u8>,
}
impl<'a> GetAssertionRequest<'a> {
pub fn encode<W: WriteBytesExt>(&self, mut encoder: &mut Encoder<W>) -> FidoResult<()> {
encoder
.writer()
.write_u8(0x02)
.context(FidoErrorKind::CborEncode)?; // authenticatorGetAssertion
let mut length = 2;
length += !self.allow_list.is_empty() as usize;
length += !self.extensions.is_empty() as usize;
length += self.options.is_some() as usize;
length += self.pin_auth.is_some() as usize;
length += self.pin_protocol.is_some() as usize;
encoder.object(length)?;
encoder.u8(0x01)?; // rpId
encoder.text(&self.rp_id)?;
encoder.u8(0x02)?; // clientDataHash
encoder.bytes(self.client_data_hash)?;
if !self.allow_list.is_empty() {
encoder.u8(0x03)?; // allowList
encoder.array(self.allow_list.len())?;
for item in self.allow_list {
item.encode(&mut encoder)?;
}
}
if self.extensions.len() > 0 {
encoder.u8(0x04)?; // extensions
encoder.object(self.extensions.len())?;
for (key, value) in self.extensions {
encoder.text(key)?;
let mut generic = GenericEncoder::new(encoder.writer());
generic.value(value)?;
}
}
if let Some(options) = &self.options {
if options.encoded() {
encoder.u8(0x05)?; // options
options.encode(&mut encoder)?;
}
}
if let Some(pin_auth) = &self.pin_auth {
encoder.u8(0x06)?; // pinAuth
encoder.bytes(pin_auth)?;
}
if let Some(pin_protocol) = &self.pin_protocol {
encoder.u8(0x07)?; // pinProtocol
encoder.u8(*pin_protocol)?;
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct GetAssertionResponse {
pub credential: Option<PublicKeyCredentialDescriptor>,
pub auth_data_bytes: Vec<u8>,
pub auth_data: AuthenticatorData,
pub signature: Vec<u8>,
}
impl GetAssertionResponse {
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
if status != 0 {
Err(FidoErrorKind::CborError(status))?
}
let mut decoder = Decoder::new(Config::default(), reader);
let mut response = GetAssertionResponse::default();
for _ in 0..decoder.object()? {
let key = decoder.u8()?;
match key {
0x01 => {
response.credential = Some(PublicKeyCredentialDescriptor::decode(&mut decoder)?)
}
0x02 => {
response.auth_data_bytes = decoder.bytes()?;
response.auth_data = AuthenticatorData::from_bytes(&response.auth_data_bytes)?;
}
0x03 => response.signature = decoder.bytes()?,
_ => continue,
}
}
Ok(response)
}
}
#[derive(Debug, Default)]
pub struct GetInfoResponse {
pub versions: Vec<String>,
pub extensions: Vec<String>,
pub aaguid: [u8; 16],
pub options: OptionsInfo,
pub max_msg_size: u16,
pub pin_protocols: Vec<u8>,
}
impl GetInfoResponse {
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
if status != 0 {
Err(FidoErrorKind::CborError(status))?
}
let mut decoder = Decoder::new(Config::default(), reader);
let mut response = GetInfoResponse::default();
for _ in 0..decoder.object()? {
match decoder.u8()? {
0x01 => {
for _ in 0..decoder.array()? {
response.versions.push(decoder.text()?);
}
}
0x02 => {
for _ in 0..decoder.array()? {
response.extensions.push(decoder.text()?);
}
}
0x03 => response.aaguid.copy_from_slice(&decoder.bytes()?[..]),
0x04 => response.options = OptionsInfo::decode(&mut decoder)?,
0x05 => response.max_msg_size = decoder.u16()?,
0x06 => {
for _ in 0..decoder.array()? {
response.pin_protocols.push(decoder.u8()?);
}
}
_ => continue,
}
}
Ok(response)
}
}
#[derive(Debug, Default)]
pub struct ClientPinRequest<'a> {
pub pin_protocol: u8,
pub sub_command: u8,
pub key_agreement: Option<&'a CoseKey>,
pub pin_auth: Option<[u8; 16]>,
pub new_pin_enc: Option<Vec<u8>>,
pub pin_hash_enc: Option<[u8; 16]>,
}
impl<'a> ClientPinRequest<'a> {
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
encoder
.writer()
.write_u8(0x06)
.context(FidoErrorKind::CborEncode)?; // authenticatorClientPIN
let mut length = 2;
length += self.key_agreement.is_some() as usize;
length += self.pin_auth.is_some() as usize;
length += self.new_pin_enc.is_some() as usize;
length += self.pin_hash_enc.is_some() as usize;
encoder.object(length)?;
encoder.u8(0x01)?; // pinProtocol
encoder.u8(self.pin_protocol)?;
encoder.u8(0x02)?; // subCommand
encoder.u8(self.sub_command)?;
if let Some(key_agreement) = self.key_agreement {
encoder.u8(0x03)?; // keyAgreement
key_agreement.encode(encoder)?;
}
if let Some(pin_auth) = &self.pin_auth {
encoder.u8(0x04)?; // pinAuth
encoder.bytes(pin_auth)?;
}
if let Some(new_pin_enc) = &self.new_pin_enc {
encoder.u8(0x05)?; // newPinEnc
encoder.bytes(&new_pin_enc)?;
}
if let Some(pin_hash_enc) = &self.pin_hash_enc {
encoder.u8(0x06)?; // pinHashEnc
encoder.bytes(pin_hash_enc)?;
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct ClientPinResponse {
pub key_agreement: Option<CoseKey>,
pub pin_token: Option<[u8; 16]>,
pub retries: Option<u8>,
}
impl ClientPinResponse {
pub fn decode<R: ReadBytesExt>(mut reader: R) -> FidoResult<Self> {
let status = reader.read_u8().context(FidoErrorKind::CborDecode)?;
if status != 0 {
Err(FidoErrorKind::CborError(status))?
}
let mut decoder = Decoder::new(Config::default(), reader);
let mut response = ClientPinResponse::default();
for _ in 0..decoder.object()? {
match decoder.u8()? {
0x01 => {
let mut generic = GenericDecoder::from_decoder(decoder);
response.key_agreement = Some(CoseKey::decode(&mut generic)?);
decoder = generic.into_inner();
}
0x02 => {
let mut pin_token = [0; 16];
pin_token.copy_from_slice(&decoder.bytes()?[..]);
response.pin_token = Some(pin_token)
}
0x03 => response.retries = Some(decoder.u8()?),
_ => continue,
}
}
Ok(response)
}
}
#[derive(Debug)]
pub struct OptionsInfo {
pub plat: bool,
pub rk: bool,
pub client_pin: Option<bool>,
pub up: bool,
pub uv: Option<bool>,
}
impl Default for OptionsInfo {
fn default() -> Self {
OptionsInfo {
plat: false,
rk: false,
client_pin: None,
up: true,
uv: None,
}
}
}
impl OptionsInfo {
pub fn decode<R: ReadBytesExt>(decoder: &mut Decoder<R>) -> FidoResult<Self> {
let mut options = OptionsInfo::default();
for _ in 0..decoder.object()? {
match decoder.text()?.as_ref() {
"plat" => options.plat = decoder.bool()?,
"rk" => options.rk = decoder.bool()?,
"clientPin" => options.client_pin = Some(decoder.bool()?),
"up" => options.up = decoder.bool()?,
"uv" => options.uv = Some(decoder.bool()?),
_ => continue,
}
}
Ok(options)
}
}
#[derive(Debug, Default)]
pub struct AuthenticatorData {
pub rp_id_hash: [u8; 32],
pub up: bool,
pub uv: bool,
pub sign_count: u32,
pub attested_credential_data: AttestedCredentialData,
pub extensions: HashMap<String, Value>,
}
impl AuthenticatorData {
pub fn from_bytes(bytes: &[u8]) -> FidoResult<Self> {
let mut data = AuthenticatorData::default();
data.rp_id_hash.copy_from_slice(&bytes[0..32]);
let flags = bytes[32];
data.up = (flags & 0x01) == 0x01;
data.uv = (flags & 0x02) == 0x02;
let is_attested = (flags & 0x40) == 0x40;
let has_extension_data = (flags & 0x80) == 0x80;
data.sign_count = BigEndian::read_u32(&bytes[33..37]);
if bytes.len() < 38 {
return Ok(data);
}
let mut cur = Cursor::new(&bytes[37..]);
if is_attested {
let attested_credential_data = AttestedCredentialData::from_bytes(&mut cur)?;
data.attested_credential_data = attested_credential_data;
if cur.position() >= (bytes.len() - 37) as u64 {
return Ok(data);
}
}
if has_extension_data {
let mut decoder = GenericDecoder::new(Config::default(), cur);
for _ in 0..decoder.borrow_mut().object()? {
let key = decoder.borrow_mut().text()?;
let value = decoder.value()?;
data.extensions.insert(key.to_string(), value);
}
}
Ok(data)
}
}
#[derive(Debug, Default)]
pub struct AttestedCredentialData {
pub aaguid: [u8; 16],
pub credential_id: Vec<u8>,
pub credential_public_key: CoseKey,
}
impl AttestedCredentialData {
pub fn from_bytes(cur: &mut Cursor<&[u8]>) -> FidoResult<Self> {
let mut response = AttestedCredentialData::default();
let bytes = cur.get_ref();
if bytes.is_empty() {
return Ok(response);
}
response.aaguid.copy_from_slice(&bytes[0..16]);
let id_length = BigEndian::read_u16(&bytes[16..18]) as usize;
response.credential_id = Vec::from(&bytes[18..(18 + id_length)]);
cur.set_position(18 + id_length as u64);
let mut decoder = GenericDecoder::new(Config::default(), cur);
response.credential_public_key = CoseKey::decode(&mut decoder)?;
Ok(response)
}
}
#[derive(Debug, Default)]
pub struct P256Key {
x: [u8; 32],
y: [u8; 32],
}
impl P256Key {
pub fn from_cose(cose: &CoseKey) -> FidoResult<Self> {
if cose.key_type != 2 || cose.algorithm != -7 {
Err(FidoErrorKind::KeyType)?
}
if let (
Some(Value::U8(curve)),
Some(Value::Bytes(value::Bytes::Bytes(x))),
Some(Value::Bytes(value::Bytes::Bytes(y))),
) = (
cose.parameters.get(&-1),
cose.parameters.get(&-2),
cose.parameters.get(&-3),
) {
if *curve != 1 {
Err(FidoErrorKind::KeyType)?
}
let mut key = P256Key::default();
key.x.copy_from_slice(&x);
key.y.copy_from_slice(&y);
return Ok(key);
}
Err(FidoErrorKind::KeyType)?
}
pub fn from_bytes(bytes: &[u8]) -> FidoResult<Self> {
if bytes.len() != 65 || bytes[0] != 0x04 {
Err(FidoErrorKind::CborDecode)?
}
let mut res = P256Key::default();
res.x.copy_from_slice(&bytes[1..33]);
res.y.copy_from_slice(&bytes[33..65]);
Ok(res)
}
pub fn to_cose(&self) -> CoseKey {
CoseKey {
key_type: 2,
algorithm: -7,
parameters: [
(-1, Value::U8(1)),
(-2, Value::Bytes(value::Bytes::Bytes(self.x.to_vec()))),
(-3, Value::Bytes(value::Bytes::Bytes(self.y.to_vec()))),
]
.iter()
.cloned()
.collect(),
}
}
pub fn bytes(&self) -> [u8; 65] {
let mut bytes = [0; 65];
bytes[0] = 0x04;
bytes[1..33].copy_from_slice(&self.x);
bytes[33..65].copy_from_slice(&self.y);
bytes
}
}
#[derive(Debug, Default)]
pub struct CoseKey {
key_type: u16,
algorithm: i32,
parameters: HashMap<i16, Value>,
}
impl CoseKey {
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
let size = 1 + self.parameters.len();
encoder.object(size)?;
encoder.i16(0x01)?; // keyType
encoder.u16(self.key_type)?;
//encoder.i16(0x02)?; // algorithm
//encoder.i32(self.algorithm)?;
for (key, value) in self.parameters.iter() {
encoder.i16(*key)?;
let mut generic = GenericEncoder::new(encoder.writer());
generic.value(value)?;
}
Ok(())
}
pub fn decode<R: ReadBytesExt>(generic: &mut GenericDecoder<R>) -> FidoResult<Self> {
let items;
{
let decoder = generic.borrow_mut();
items = decoder.object()?;
}
let mut cose_key = CoseKey::default();
cose_key.algorithm = -7;
for _ in 0..items {
match generic.borrow_mut().i16()? {
0x01 => cose_key.key_type = generic.borrow_mut().u16()?,
0x02 => cose_key.algorithm = generic.borrow_mut().i32()?,
key if key < 0 => {
cose_key.parameters.insert(key, generic.value()?);
}
_ => {
generic.value()?; // skip unknown parameter
}
}
}
Ok(cose_key)
}
}
#[derive(Debug, Default)]
pub struct PublicKeyCredentialRpEntity<'a> {
pub id: &'a str,
pub name: Option<&'a str>,
pub icon: Option<&'a str>,
}
impl<'a> PublicKeyCredentialRpEntity<'a> {
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
let mut length = 1;
length += self.name.is_some() as usize;
length += self.icon.is_some() as usize;
encoder.object(length)?;
encoder.text("id")?;
encoder.text(&self.id)?;
if let Some(icon) = &self.icon {
encoder.text("icon")?;
encoder.text(&icon)?;
}
if let Some(name) = &self.name {
encoder.text("name")?;
encoder.text(&name)?;
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct PublicKeyCredentialUserEntity<'a> {
pub id: &'a [u8],
pub name: &'a str,
pub icon: Option<&'a str>,
pub display_name: Option<&'a str>,
}
impl<'a> PublicKeyCredentialUserEntity<'a> {
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
let mut length = 2;
length += self.icon.is_some() as usize;
length += self.display_name.is_some() as usize;
encoder.object(length)?;
encoder.text("id")?;
encoder.bytes(&self.id)?;
if let Some(icon) = &self.icon {
encoder.text("icon")?;
encoder.text(&icon)?;
}
encoder.text("name")?;
encoder.text(&self.name)?;
if let Some(display_name) = &self.display_name {
encoder.text("displayName")?;
encoder.text(&display_name)?;
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct PublicKeyCredentialDescriptor {
pub cred_type: String,
pub id: Vec<u8>,
}
impl PublicKeyCredentialDescriptor {
pub fn decode<R: ReadBytesExt>(decoder: &mut Decoder<R>) -> FidoResult<Self> {
let mut response = PublicKeyCredentialDescriptor::default();
for _ in 0..decoder.object()? {
match decoder.text()?.as_ref() {
"id" => response.id = decoder.bytes()?,
"type" => response.cred_type = decoder.text()?,
_ => continue,
}
}
Ok(response)
}
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
encoder.object(2)?;
encoder.text("id")?;
encoder.bytes(&self.id)?;
encoder.text("type")?;
encoder.text(&self.cred_type)?;
Ok(())
}
}
#[derive(Debug)]
pub struct AuthenticatorOptions {
pub rk: bool,
pub uv: bool,
}
impl AuthenticatorOptions {
pub fn encoded(&self) -> bool {
self.rk || self.uv
}
pub fn encode<W: WriteBytesExt>(&self, encoder: &mut Encoder<W>) -> FidoResult<()> {
let length = (self.rk as usize) + (self.uv as usize);
encoder.object(length)?;
if self.rk {
encoder.text("rk")?;
encoder.bool(true)?;
}
if self.uv {
encoder.text("uv")?;
encoder.bool(true)?;
}
Ok(())
}
}

View File

@ -1,130 +0,0 @@
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use super::cbor::{CoseKey, P256Key};
use super::error::*;
use failure::ResultExt;
use ring::error::Unspecified;
use ring::{agreement, digest, hmac, rand, signature};
use rust_crypto::aes;
use rust_crypto::blockmodes::NoPadding;
use rust_crypto::buffer::{RefReadBuffer, RefWriteBuffer};
use rust_crypto::symmetriccipher::{Decryptor, Encryptor};
use untrusted::Input;
#[derive(Debug)]
pub struct SharedSecret {
pub public_key: CoseKey,
pub shared_secret: [u8; 32],
}
impl SharedSecret {
pub fn new(peer_key: &CoseKey) -> FidoResult<Self> {
let rng = rand::SystemRandom::new();
let private = agreement::EphemeralPrivateKey::generate(&agreement::ECDH_P256, &rng)
.context(FidoErrorKind::GenerateKey)?;
let public = &mut [0u8; agreement::PUBLIC_KEY_MAX_LEN][..private.public_key_len()];
private
.compute_public_key(public)
.context(FidoErrorKind::GenerateKey)?;
let peer = P256Key::from_cose(peer_key)
.context(FidoErrorKind::ParsePublic)?
.bytes();
let peer = Input::from(&peer);
let shared_secret = agreement::agree_ephemeral(
private,
&agreement::ECDH_P256,
peer,
Unspecified,
|material| Ok(digest::digest(&digest::SHA256, material)),
)
.context(FidoErrorKind::GenerateSecret)?;
let mut res = SharedSecret {
public_key: P256Key::from_bytes(&public)
.context(FidoErrorKind::ParsePublic)?
.to_cose(),
shared_secret: [0; 32],
};
res.shared_secret.copy_from_slice(shared_secret.as_ref());
Ok(res)
}
pub fn encryptor(&self) -> Box<dyn Encryptor + 'static> {
aes::cbc_encryptor(
aes::KeySize::KeySize256,
&self.shared_secret,
&[0u8; 16],
NoPadding,
)
}
pub fn encrypt_pin(&self, pin: &str) -> FidoResult<[u8; 16]> {
let mut encryptor = self.encryptor();
let pin_bytes = pin.as_bytes();
let hash = digest::digest(&digest::SHA256, &pin_bytes);
let in_bytes = &hash.as_ref()[0..16];
let mut input = RefReadBuffer::new(&in_bytes);
let mut out_bytes = [0; 16];
let mut output = RefWriteBuffer::new(&mut out_bytes);
encryptor
.encrypt(&mut input, &mut output, true)
.map_err(|_| FidoErrorKind::EncryptPin)?;
Ok(out_bytes)
}
pub fn decryptor(&self) -> Box<dyn Decryptor + 'static> {
aes::cbc_decryptor(
aes::KeySize::KeySize256,
&self.shared_secret,
&[0u8; 16],
NoPadding,
)
}
pub fn decrypt_token(&self, data: &mut [u8]) -> FidoResult<PinToken> {
let mut decryptor = self.decryptor();
let mut input = RefReadBuffer::new(data);
let mut out_bytes = [0; 16];
let mut output = RefWriteBuffer::new(&mut out_bytes);
decryptor
.decrypt(&mut input, &mut output, true)
.map_err(|_| FidoErrorKind::DecryptPin)?;
Ok(PinToken(hmac::SigningKey::new(&digest::SHA256, &out_bytes)))
}
}
pub struct PinToken(hmac::SigningKey);
impl PinToken {
pub fn auth(&self, data: &[u8]) -> [u8; 16] {
let signature = hmac::sign(&self.0, &data);
let mut out = [0; 16];
out.copy_from_slice(&signature.as_ref()[0..16]);
out
}
}
pub fn verify_signature(
public_key: &[u8],
client_data: &[u8],
auth_data: &[u8],
signature: &[u8],
) -> bool {
let public_key = Input::from(&public_key);
let msg_len = client_data.len() + auth_data.len();
let mut msg = Vec::with_capacity(msg_len);
msg.extend_from_slice(auth_data);
msg.extend_from_slice(client_data);
let msg = Input::from(&msg);
let signature = Input::from(signature);
signature::verify(
&signature::ECDSA_P256_SHA256_ASN1,
public_key,
msg,
signature,
)
.is_ok()
}

View File

@ -1,101 +0,0 @@
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use cbor_codec::{DecodeError, EncodeError};
use failure::{Backtrace, Context, Fail};
use std::fmt;
use std::fmt::Display;
pub type FidoResult<T> = Result<T, FidoError>;
#[derive(Debug)]
pub struct FidoError(Context<FidoErrorKind>);
#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)]
pub enum FidoErrorKind {
#[fail(display = "Read/write error with device.")]
Io,
#[fail(display = "Error while reading packet from device.")]
ReadPacket,
#[fail(display = "Error while writing packet to device.")]
WritePacket,
#[fail(display = "Error while parsing CTAP from device.")]
ParseCtap,
#[fail(display = "Error while encoding CBOR for device.")]
CborEncode,
#[fail(display = "Error while decoding CBOR from device.")]
CborDecode,
#[fail(display = "Packets received from device in the wrong order.")]
InvalidSequence,
#[fail(display = "Failed to generate private keypair.")]
GenerateKey,
#[fail(display = "Failed to generate shared secret.")]
GenerateSecret,
#[fail(display = "Failed to parse public key.")]
ParsePublic,
#[fail(display = "Failed to encrypt PIN.")]
EncryptPin,
#[fail(display = "Failed to decrypt PIN.")]
DecryptPin,
#[fail(display = "Supplied key has incorrect type.")]
KeyType,
#[fail(display = "Device returned error: 0x{:x}", _0)]
CborError(u8),
#[fail(display = "Device does not support FIDO2")]
DeviceUnsupported,
#[fail(display = "This operating requires a PIN but none was provided.")]
PinRequired,
}
impl Fail for FidoError {
fn cause(&self) -> Option<&dyn Fail> {
self.0.cause()
}
fn backtrace(&self) -> Option<&Backtrace> {
self.0.backtrace()
}
}
impl Display for FidoError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl FidoError {
pub fn kind(&self) -> FidoErrorKind {
*self.0.get_context()
}
}
impl From<FidoErrorKind> for FidoError {
#[inline(always)]
fn from(kind: FidoErrorKind) -> FidoError {
FidoError(Context::new(kind))
}
}
impl From<Context<FidoErrorKind>> for FidoError {
fn from(inner: Context<FidoErrorKind>) -> FidoError {
FidoError(inner)
}
}
impl From<EncodeError> for FidoError {
#[inline(always)]
fn from(err: EncodeError) -> FidoError {
FidoError(err.context(FidoErrorKind::CborEncode))
}
}
impl From<DecodeError> for FidoError {
#[inline(always)]
fn from(err: DecodeError) -> FidoError {
FidoError(err.context(FidoErrorKind::CborDecode))
}
}

View File

@ -1,200 +0,0 @@
use crate::cbor;
use crate::{FidoCredential, FidoDevice, FidoErrorKind, FidoResult};
use cbor_codec::value::{Bytes, Int, Key, Text, Value};
use cbor_codec::Encoder;
use cbor_codec::{Config, GenericDecoder};
use rust_crypto::buffer::{RefReadBuffer, RefWriteBuffer};
use rust_crypto::digest::Digest;
use rust_crypto::hmac::Hmac;
use rust_crypto::mac::Mac;
use rust_crypto::sha2::Sha256;
use std::collections::BTreeMap;
use std::io::Cursor;
#[derive(Debug, Clone)]
pub struct FidoHmacCredential {
pub id: Vec<u8>,
pub rp_id: String,
}
impl From<FidoCredential> for FidoHmacCredential {
fn from(cred: FidoCredential) -> Self {
FidoHmacCredential {
id: cred.id,
rp_id: cred.rp_id,
}
}
}
pub trait HmacExtension {
fn extension_name() -> &'static str {
"hmac-secret"
}
fn get_dict(&mut self, salt: &[u8; 32], salt2: Option<&[u8; 32]>) -> FidoResult<Value> {
let mut map = BTreeMap::new();
map.insert(
Key::Text(Text::Text(Self::extension_name().to_owned())),
self.get_data(salt, salt2)?,
);
Ok(Value::Map(map))
}
fn get_data(&mut self, salt: &[u8; 32], salt2: Option<&[u8; 32]>) -> FidoResult<Value>;
fn make_hmac_credential(&mut self) -> FidoResult<FidoHmacCredential>;
fn get_hmac_assertion(
&mut self,
credential: &FidoHmacCredential,
salt: &[u8; 32],
salt2: Option<&[u8; 32]>,
) -> FidoResult<([u8; 32], Option<[u8; 32]>)>;
fn hmac_challange(
&mut self,
credential: &FidoHmacCredential,
input: &[u8],
) -> FidoResult<[u8; 32]> {
let mut salt = [0u8; 32];
let mut digest = Sha256::new();
digest.input(input);
digest.result(&mut salt);
self.get_hmac_assertion(credential, &salt, None)
.map(|secret| secret.0)
}
}
impl HmacExtension for FidoDevice {
fn get_data(&mut self, salt: &[u8; 32], salt2: Option<&[u8; 32]>) -> FidoResult<Value> {
let shared_secret = self.shared_secret.as_ref().unwrap();
let mut encryptor = shared_secret.encryptor();
let mut salt_enc = [0u8; 64];
let mut output = RefWriteBuffer::new(&mut salt_enc);
let mut encrypt = || {
encryptor.encrypt(&mut RefReadBuffer::new(salt), &mut output, salt2.is_none())?;
if let Some(salt2) = salt2 {
encryptor
.encrypt(&mut RefReadBuffer::new(salt2), &mut output, true)
.map(|_| ())
} else {
Ok(())
}
};
encrypt().map_err(|_| FidoErrorKind::Io)?;
let key_agreement = || {
let mut cur = Cursor::new(Vec::new());
let mut encoder = Encoder::new(&mut cur);
shared_secret.public_key.encode(&mut encoder).unwrap();
cur.set_position(0);
let mut dec = GenericDecoder::new(Config::default(), cur);
dec.value()
};
let mut map = BTreeMap::new();
map.insert(
Key::Int(Int::from_i64(0x01)),
key_agreement().map_err(|_| FidoErrorKind::Io)?,
);
map.insert(
Key::Int(Int::from_i64(0x02)),
Value::Bytes(Bytes::Bytes(
salt_enc[0..((salt2.is_some() as usize + 1) * 32)].to_vec(),
)),
);
let mut salt_hmac = Hmac::new(Sha256::new(), &shared_secret.shared_secret);
salt_hmac.input(&salt_enc[0..((salt2.is_some() as usize + 1) * 32)]);
let mut authed_salt_enc = [0u8; 32];
authed_salt_enc.copy_from_slice(salt_hmac.result().code());
map.insert(
Key::Int(Int::from_i64(0x03)),
Value::Bytes(Bytes::Bytes(authed_salt_enc[0..16].to_vec())),
);
Ok(Value::Map(map))
}
fn make_hmac_credential(&mut self) -> FidoResult<FidoHmacCredential> {
self.make_credential("hmac", &[0u8], "commandline", &[0u8; 32])
.map(|cred| cred.into())
}
fn get_hmac_assertion(
&mut self,
credential: &FidoHmacCredential,
salt: &[u8; 32],
salt2: Option<&[u8; 32]>,
) -> FidoResult<([u8; 32], Option<[u8; 32]>)> {
let client_data_hash = [0u8; 32];
while self.shared_secret.is_none() {
self.init_shared_secret()?;
}
if self.needs_pin && self.pin_token.is_none() {
Err(FidoErrorKind::PinRequired)?
}
if client_data_hash.len() != 32 {
Err(FidoErrorKind::CborEncode)?
}
let pin_auth = self
.pin_token
.as_ref()
.map(|token| token.auth(&client_data_hash));
let ext_data: Value = self.get_data(salt, salt2)?;
let allow_list = [cbor::PublicKeyCredentialDescriptor {
cred_type: String::from("public-key"),
id: credential.id.clone(),
}];
let request = cbor::GetAssertionRequest {
rp_id: &credential.rp_id,
client_data_hash: &client_data_hash,
allow_list: &allow_list,
extensions: &[(<Self as HmacExtension>::extension_name(), &ext_data)],
options: Some(cbor::AuthenticatorOptions {
rk: false,
uv: true,
}),
pin_auth,
pin_protocol: pin_auth.and(Some(0x01)),
};
let response = match self.cbor(cbor::Request::GetAssertion(request))? {
cbor::Response::GetAssertion(resp) => resp,
_ => Err(FidoErrorKind::CborDecode)?,
};
let shared_secret = self.shared_secret.as_ref().unwrap();
let mut decryptor = shared_secret.decryptor();
let mut hmac_secret_combined = [0u8; 64];
let _output = RefWriteBuffer::new(&mut hmac_secret_combined);
let hmac_secret_enc = match response
.auth_data
.extensions
.get(<Self as HmacExtension>::extension_name())
.ok_or(FidoErrorKind::CborDecode)?
{
Value::Bytes(hmac_ciphered) => Ok(match hmac_ciphered {
Bytes::Bytes(hmac_ciphered) => hmac_ciphered.to_vec(),
Bytes::Chunks(hmac_ciphered) => hmac_ciphered.iter().fold(Vec::new(), |s, i| {
let mut s = s;
s.extend_from_slice(&i);
s
}),
}),
_ => Err(FidoErrorKind::CborDecode),
}?;
let mut hmac_secret = ([0u8; 32], [0u8; 32]);
decryptor
.decrypt(
&mut RefReadBuffer::new(&hmac_secret_enc),
&mut RefWriteBuffer::new(unsafe {
std::mem::transmute::<_, &mut [u8; 64]>(&mut hmac_secret)
}),
true,
)
.expect("failed to decrypt secret");
Ok((hmac_secret.0, salt2.map(|_| hmac_secret.1)))
}
}

View File

@ -1 +0,0 @@
pub mod hmac;

View File

@ -1,16 +0,0 @@
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use std::path::PathBuf;
#[derive(Debug, Clone)]
/// Storage for device related information
pub struct DeviceInfo {
pub path: PathBuf,
pub usage_page: u16,
pub usage: u16,
pub report_size: u16,
}

View File

@ -1,88 +0,0 @@
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
pub use super::hid_common::*;
use byteorder::{ByteOrder, LittleEndian};
use std::fs;
use std::io;
use std::path::PathBuf;
static REPORT_DESCRIPTOR_KEY_MASK: u8 = 0xfc;
static LONG_ITEM_ENCODING: u8 = 0xfe;
static USAGE_PAGE: u8 = 0x04;
static USAGE: u8 = 0x08;
static REPORT_SIZE: u8 = 0x74;
pub fn enumerate() -> io::Result<impl Iterator<Item = DeviceInfo>> {
fs::read_dir("/sys/class/hidraw").map(|entries| {
entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| path_to_device(&entry.path()).ok())
})
}
fn path_to_device(path: &PathBuf) -> io::Result<DeviceInfo> {
let mut rd_path = path.clone();
rd_path.push("device/report_descriptor");
let rd = fs::read(rd_path)?;
let mut usage_page: u16 = 0;
let mut usage: u16 = 0;
let mut report_size: u16 = 0;
let mut pos: usize = 0;
while pos < rd.len() {
let key = rd[pos];
let mut key_size: usize = 1;
let mut size: u8;
if key == LONG_ITEM_ENCODING {
key_size = 3;
size = rd[pos + 1];
} else {
size = key & 0x03;
if size == 0x03 {
size = 0x04
}
}
if key & REPORT_DESCRIPTOR_KEY_MASK == USAGE_PAGE {
if size != 2 {
usage_page = u16::from(rd[pos + 1])
} else {
usage_page = LittleEndian::read_u16(&rd[(pos + 1)..(pos + 1 + (size as usize))]);
}
}
if key & REPORT_DESCRIPTOR_KEY_MASK == USAGE {
if size != 2 {
usage = u16::from(rd[pos + 1])
} else {
usage = LittleEndian::read_u16(&rd[(pos + 1)..(pos + 1 + (size as usize))]);
}
}
if key & REPORT_DESCRIPTOR_KEY_MASK == REPORT_SIZE {
if size != 2 {
report_size = u16::from(rd[pos + 1])
} else {
report_size = LittleEndian::read_u16(&rd[(pos + 1)..(pos + 1 + (size as usize))]);
}
}
pos = pos + key_size + size as usize;
}
let mut device_path = PathBuf::from("/dev");
device_path.push(path.file_name().unwrap());
Ok(DeviceInfo {
path: device_path,
usage_page,
usage,
report_size,
})
}

View File

@ -1,402 +0,0 @@
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
//! An implementation of the CTAP2 protocol over USB.
//!
//! # Example
//!
//! ```
//! # fn do_fido() -> ctap::FidoResult<()> {
//! let mut devices = ctap::get_devices()?;
//! let device_info = &devices.next().unwrap();
//! let mut device = ctap::FidoDevice::new(device_info)?;
//!
//! // This can be omitted if the FIDO device is not configured with a PIN.
//! let pin = "test";
//! device.unlock(pin)?;
//!
//! // In a real application these values would come from the requesting app.
//! let rp_id = "rp_id";
//! let user_id = [0];
//! let user_name = "user_name";
//! let client_data_hash = [0; 32];
//! let cred = device.make_credential(
//! rp_id,
//! &user_id,
//! user_name,
//! &client_data_hash
//! )?;
//!
//! // In a real application the credential would be stored and used later.
//! let result = device.get_assertion(&cred, &client_data_hash);
//! # Ok(())
//! # }
#![allow(dead_code)]
extern crate failure;
extern crate rand;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate num_derive;
extern crate byteorder;
extern crate cbor as cbor_codec;
extern crate crypto as rust_crypto;
extern crate num_traits;
extern crate ring;
extern crate untrusted;
mod cbor;
mod crypto;
mod error;
pub mod extensions;
mod hid_common;
mod hid_linux;
mod packet;
use std::cmp;
use std::fs;
use std::io::{Cursor, Write};
use std::u16;
use std::u8;
pub use self::error::*;
use self::hid_linux as hid;
use self::packet::CtapCommand;
use failure::{Fail, ResultExt};
use num_traits::FromPrimitive;
use rand::prelude::*;
static BROADCAST_CID: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
/// Looks for any connected HID devices and returns those that support FIDO.
pub fn get_devices() -> FidoResult<impl Iterator<Item = hid::DeviceInfo>> {
hid::enumerate()
.context(FidoErrorKind::Io)
.map(|devices| devices.filter(|dev| dev.usage_page == 0xf1d0 && dev.usage == 0x21))
.map_err(From::from)
}
/// A credential created by a FIDO2 authenticator.
#[derive(Debug)]
pub struct FidoCredential {
/// The ID provided by the authenticator.
pub id: Vec<u8>,
/// The public key provided by the authenticator, in uncompressed form.
pub public_key: Vec<u8>,
/// The Relying Party ID provided by the platform when this key was generated.
pub rp_id: String,
}
/// An opened FIDO authenticator.
pub struct FidoDevice {
device: fs::File,
packet_size: u16,
channel_id: [u8; 4],
needs_pin: bool,
shared_secret: Option<crypto::SharedSecret>,
pin_token: Option<crypto::PinToken>,
aaguid: [u8; 16],
}
impl FidoDevice {
/// Open and initialize a given device. DeviceInfo is provided by the `get_devices`
/// function. This method will allocate a channel for this application, verify that
/// it supports FIDO2, and checks if a PIN is set.
///
/// This method will fail if the device can't be opened, if the device returns
/// malformed data or if the device is not supported.
pub fn new(device: &hid::DeviceInfo) -> error::FidoResult<Self> {
let mut options = fs::OpenOptions::new();
options.read(true).write(true);
let mut dev = FidoDevice {
device: options.open(&device.path).context(FidoErrorKind::Io)?,
packet_size: 64,
channel_id: BROADCAST_CID,
needs_pin: false,
shared_secret: None,
pin_token: None,
aaguid: [0; 16],
};
dev.init()?;
Ok(dev)
}
fn init(&mut self) -> FidoResult<()> {
let mut nonce = [0u8; 8];
thread_rng().fill_bytes(&mut nonce);
let response = self.exchange(CtapCommand::Init, &nonce)?;
if response.len() < 17 || response[0..8] != nonce {
Err(FidoErrorKind::ParseCtap)?
}
let flags = response[16];
if flags & 0x04 == 0 {
Err(FidoErrorKind::DeviceUnsupported)?
}
self.channel_id.copy_from_slice(&response[8..12]);
let response = match self.cbor(cbor::Request::GetInfo)? {
cbor::Response::GetInfo(resp) => resp,
_ => Err(FidoErrorKind::CborDecode)?,
};
if !response.versions.iter().any(|ver| ver == "FIDO_2_0") {
Err(FidoErrorKind::DeviceUnsupported)?
}
if !response.pin_protocols.iter().any(|ver| *ver == 1) {
Err(FidoErrorKind::DeviceUnsupported)?
}
self.needs_pin = response.options.client_pin == Some(true);
self.aaguid = response.aaguid;
Ok(())
}
/// Get the authenticator's AAGUID. This is not unique to an authenticator,
/// but it is unique to the specific brand and model.
pub fn aaguid(&self) -> &[u8] {
&self.aaguid
}
fn init_shared_secret(&mut self) -> FidoResult<()> {
let mut request = cbor::ClientPinRequest::default();
request.pin_protocol = 1;
request.sub_command = 0x02; // getKeyAgreement
let response = match self.cbor(cbor::Request::ClientPin(request))? {
cbor::Response::ClientPin(resp) => resp,
_ => Err(FidoErrorKind::CborDecode)?,
};
if let Some(key_agreement) = response.key_agreement {
self.shared_secret = Some(crypto::SharedSecret::new(&key_agreement)?);
Ok(())
} else {
Err(FidoErrorKind::CborDecode)?
}
}
/// Unlock the device with the provided PIN. Internally this will generate
/// an ECDH keypair, send the encrypted PIN to the device and store the PIN
/// token that the device generates on every power cycle. The PIN itself is
/// not stored.
///
/// This method will fail if the device returns malformed data or the PIN is
/// incorrect.
pub fn unlock(&mut self, pin: &str) -> FidoResult<()> {
while self.shared_secret.is_none() {
self.init_shared_secret()?;
}
// If the PIN is invalid the device should create a new agreementKey,
// so we only replace shared_secret on success.
let shared_secret = self.shared_secret.take().unwrap();
let mut request = cbor::ClientPinRequest::default();
request.pin_protocol = 1;
request.sub_command = 0x05; // getPINToken
request.key_agreement = Some(&shared_secret.public_key);
request.pin_hash_enc = Some(shared_secret.encrypt_pin(pin)?);
let response = match self.cbor(cbor::Request::ClientPin(request))? {
cbor::Response::ClientPin(resp) => resp,
_ => Err(FidoErrorKind::CborDecode)?,
};
if let Some(mut pin_token) = response.pin_token {
self.pin_token = Some(shared_secret.decrypt_token(&mut pin_token)?);
self.shared_secret = Some(shared_secret);
Ok(())
} else {
Err(FidoErrorKind::CborDecode)?
}
}
/// Request a new credential from the authenticator. The `rp_id` should be
/// a stable string used to identify the party for whom the credential is
/// created, for convenience it will be returned with the credential.
/// `user_id` and `user_name` are not required when requesting attestations
/// but they MAY be displayed to the user and MAY be stored on the device
/// to be returned with an attestation if the device supports this.
/// `client_data_hash` SHOULD be a SHA256 hash of provided `client_data`,
/// this is only used to verify the attestation provided by the
/// authenticator. When not implementing WebAuthN this can be any random
/// 32-byte array.
///
/// This method will fail if a PIN is required but the device is not
/// unlocked or if the device returns malformed data.
pub fn make_credential(
&mut self,
rp_id: &str,
user_id: &[u8],
user_name: &str,
client_data_hash: &[u8],
) -> FidoResult<FidoCredential> {
if self.needs_pin && self.pin_token.is_none() {
Err(FidoErrorKind::PinRequired)?
}
if client_data_hash.len() != 32 {
Err(FidoErrorKind::CborEncode)?
}
let pin_auth = self
.pin_token
.as_ref()
.map(|token| token.auth(&client_data_hash));
let rp = cbor::PublicKeyCredentialRpEntity {
id: rp_id,
name: None,
icon: None,
};
let user = cbor::PublicKeyCredentialUserEntity {
id: user_id,
name: user_name,
icon: None,
display_name: None,
};
let pub_key_cred_params = [("public-key", -7)];
let request = cbor::MakeCredentialRequest {
client_data_hash,
rp,
user,
pub_key_cred_params: &pub_key_cred_params,
exclude_list: Default::default(),
extensions: Default::default(),
options: Some(cbor::AuthenticatorOptions {
rk: false,
uv: true,
}),
pin_auth,
pin_protocol: pin_auth.and(Some(0x01)),
};
let response = match self.cbor(cbor::Request::MakeCredential(request))? {
cbor::Response::MakeCredential(resp) => resp,
_ => Err(FidoErrorKind::CborDecode)?,
};
let public_key = cbor::P256Key::from_cose(
&response
.auth_data
.attested_credential_data
.credential_public_key,
)?
.bytes();
Ok(FidoCredential {
id: response.auth_data.attested_credential_data.credential_id,
rp_id: String::from(rp_id),
public_key: Vec::from(&public_key[..]),
})
}
/// Request an assertion from the authenticator for a given credential.
/// `client_data_hash` SHOULD be a SHA256 hash of provided `client_data`,
/// this is signed and verified as part of the attestation. When not
/// implementing WebAuthN this can be any random 32-byte array.
///
/// This method will return whether the assertion matches the credential
/// provided, and will fail if a PIN is required but not provided or if the
/// device returns malformed data.
pub fn get_assertion(
&mut self,
credential: &FidoCredential,
client_data_hash: &[u8],
) -> FidoResult<bool> {
if self.needs_pin && self.pin_token.is_none() {
Err(FidoErrorKind::PinRequired)?
}
if client_data_hash.len() != 32 {
Err(FidoErrorKind::CborEncode)?
}
let pin_auth = self
.pin_token
.as_ref()
.map(|token| token.auth(&client_data_hash));
let allow_list = [cbor::PublicKeyCredentialDescriptor {
cred_type: String::from("public-key"),
id: credential.id.clone(),
}];
let request = cbor::GetAssertionRequest {
rp_id: &credential.rp_id,
client_data_hash: client_data_hash,
allow_list: &allow_list,
extensions: Default::default(),
options: Some(cbor::AuthenticatorOptions {
rk: false,
uv: true,
}),
pin_auth,
pin_protocol: pin_auth.and(Some(0x01)),
};
let response = match self.cbor(cbor::Request::GetAssertion(request))? {
cbor::Response::GetAssertion(resp) => resp,
_ => Err(FidoErrorKind::CborDecode)?,
};
Ok(crypto::verify_signature(
&credential.public_key,
&client_data_hash,
&response.auth_data_bytes,
&response.signature,
))
}
fn cbor(&mut self, request: cbor::Request) -> FidoResult<cbor::Response> {
let mut buf = Cursor::new(Vec::new());
request
.encode(&mut buf)
.context(FidoErrorKind::CborEncode)?;
let response = self.exchange(CtapCommand::Cbor, &buf.into_inner())?;
request
.decode(Cursor::new(response))
.context(FidoErrorKind::CborDecode)
.map_err(From::from)
}
fn exchange(&mut self, cmd: CtapCommand, payload: &[u8]) -> FidoResult<Vec<u8>> {
self.send(&cmd, payload)?;
self.receive(&cmd)
}
fn send(&mut self, cmd: &CtapCommand, payload: &[u8]) -> FidoResult<()> {
if payload.is_empty() || payload.len() > u16::MAX as usize {
Err(FidoErrorKind::WritePacket)?
}
let to_send = payload.len() as u16;
let max_payload = (self.packet_size - 7) as usize;
let (frame, payload) = payload.split_at(cmp::min(payload.len(), max_payload));
packet::write_init_packet(&mut self.device, 64, &self.channel_id, cmd, to_send, frame)?;
if payload.is_empty() {
return Ok(());
}
let max_payload = (self.packet_size - 5) as usize;
for (seq, frame) in (0..u8::MAX).zip(payload.chunks(max_payload)) {
packet::write_cont_packet(&mut self.device, 64, &self.channel_id, seq, frame)?;
}
self.device.flush().context(FidoErrorKind::WritePacket)?;
Ok(())
}
fn receive(&mut self, cmd: &CtapCommand) -> FidoResult<Vec<u8>> {
let mut first_packet: Option<packet::InitPacket> = None;
while first_packet.is_none() {
let packet = packet::InitPacket::from_reader(&mut self.device, 64)?;
if packet.cmd == CtapCommand::Error {
Err(packet::CtapError::from_u8(packet.payload[0])
.unwrap_or(packet::CtapError::Other)
.context(FidoErrorKind::ParseCtap))?
}
if packet.cid == self.channel_id && &packet.cmd == cmd {
first_packet = Some(packet);
}
}
let first_packet = first_packet.unwrap();
let mut data = first_packet.payload;
let mut to_read = (first_packet.size as isize) - data.len() as isize;
let mut seq = 0;
while to_read > 0 {
let packet = packet::ContPacket::from_reader(&mut self.device, 64, to_read as usize)?;
if packet.cid != self.channel_id {
continue;
}
if packet.seq != seq {
Err(FidoErrorKind::InvalidSequence)?
}
to_read -= packet.payload.len() as isize;
data.extend(&packet.payload);
seq += 1;
}
Ok(data)
}
}

View File

@ -1,179 +0,0 @@
// This file is part of ctap, a Rust implementation of the FIDO2 protocol.
// Copyright (c) Ariën Holthuizen <contact@ardaxi.com>
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
use super::error::*;
use failure::ResultExt;
use num_traits::{FromPrimitive, ToPrimitive};
use std::io::{Read, Write};
static FRAME_INIT: u8 = 0x80;
#[repr(u8)]
#[derive(FromPrimitive, ToPrimitive, PartialEq)]
pub enum CtapCommand {
Invalid = 0x00,
Ping = 0x01,
Msg = 0x03,
Lock = 0x04,
Init = 0x06,
Wink = 0x08,
Cbor = 0x10,
Cancel = 0x11,
Keepalive = 0x3b,
Error = 0x3f,
}
impl CtapCommand {
pub fn to_wire_format(&self) -> u8 {
match self.to_u8() {
Some(x) => x,
None => 0x00,
}
}
}
#[repr(u8)]
#[derive(FromPrimitive, Fail, Debug)]
pub enum CtapError {
#[fail(display = "The command in the request is invalid")]
InvalidCmd = 0x01,
#[fail(display = "The parameter(s) in the request is invalid")]
InvalidPar = 0x02,
#[fail(display = "The length field (BCNT) is invalid for the request ")]
InvalidLen = 0x03,
#[fail(display = "The sequence does not match expected value ")]
InvalidSeq = 0x04,
#[fail(display = "The message has timed out ")]
MsgTimeout = 0x05,
#[fail(display = "The device is busy for the requesting channel ")]
ChannelBusy = 0x06,
#[fail(display = "Command requires channel lock ")]
LockRequired = 0x0A,
#[fail(display = "Reserved error")]
NA = 0x0B,
#[fail(display = "Unspecified error")]
Other = 0x7F,
}
pub fn write_init_packet<W: Write>(
mut writer: W,
report_size: usize,
cid: &[u8],
cmd: &CtapCommand,
size: u16,
payload: &[u8],
) -> FidoResult<()> {
if cid.len() != 4 {
Err(FidoErrorKind::WritePacket)?
}
let mut packet = Vec::with_capacity(report_size);
packet.push(0);
packet.extend_from_slice(cid);
packet.push(FRAME_INIT | cmd.to_wire_format());
packet.push(((size >> 8) & 0xff) as u8);
packet.push((size & 0xff) as u8);
packet.extend_from_slice(payload);
if packet.len() > report_size + 1 {
Err(FidoErrorKind::WritePacket)?
}
packet.resize(report_size + 1, 0);
writer
.write_all(&packet)
.context(FidoErrorKind::WritePacket)?;
Ok(())
}
pub struct InitPacket {
pub cid: [u8; 4],
pub cmd: CtapCommand,
pub size: u16,
pub payload: Vec<u8>,
}
impl InitPacket {
pub fn from_reader<R: Read>(mut reader: R, report_size: usize) -> FidoResult<InitPacket> {
let mut buf = Vec::with_capacity(report_size);
buf.resize(report_size, 0);
reader
.read_exact(&mut buf[0..report_size])
.context(FidoErrorKind::ReadPacket)?;
let mut cid = [0; 4];
cid.copy_from_slice(&buf[0..4]);
let cmd = match CtapCommand::from_u8(buf[4] ^ FRAME_INIT) {
Some(cmd) => cmd,
None => CtapCommand::Invalid,
};
let size = ((u16::from(buf[5])) << 8) | u16::from(buf[6]);
let payload_end = if (size as usize) >= (report_size - 7) {
report_size
} else {
size as usize + 7
};
let payload = buf.drain(7..payload_end).collect();
Ok(InitPacket {
cid,
cmd,
size,
payload,
})
}
}
pub fn write_cont_packet<W: Write>(
mut writer: W,
report_size: usize,
cid: &[u8],
seq: u8,
payload: &[u8],
) -> FidoResult<()> {
if cid.len() != 4 {
Err(FidoErrorKind::WritePacket)?
}
let mut packet = Vec::with_capacity(report_size);
packet.push(0);
packet.extend_from_slice(cid);
packet.push(seq);
packet.extend_from_slice(payload);
if packet.len() > report_size + 1 {
Err(FidoErrorKind::WritePacket)?
}
packet.resize(report_size + 1, 0);
writer
.write_all(&packet)
.context(FidoErrorKind::WritePacket)?;
Ok(())
}
pub struct ContPacket {
pub cid: [u8; 4],
pub seq: u8,
pub payload: Vec<u8>,
}
impl ContPacket {
pub fn from_reader<R: Read>(
mut reader: R,
report_size: usize,
expected_data: usize,
) -> FidoResult<ContPacket> {
let mut buf = Vec::with_capacity(report_size);
buf.resize(report_size, 0);
reader
.read_exact(&mut buf[0..report_size])
.context(FidoErrorKind::ReadPacket)?;
let mut cid = [0; 4];
cid.copy_from_slice(&buf[0..4]);
let seq = buf[4];
let payload_end = if expected_data >= (report_size - 5) {
report_size
} else {
expected_data + 5
};
let payload = buf.drain(5..payload_end).collect();
Ok(ContPacket { cid, seq, payload })
}
}

View File

@ -1,264 +1,339 @@
use crate::error::*;
use crate::luks::{Fido2LuksToken, LuksDevice};
use crate::util::sha256;
use crate::*;
use cli_args::*;
use cryptsetup_rs as luks;
use cryptsetup_rs::api::{CryptDeviceHandle, CryptDeviceOpenBuilder, Luks1Params};
use cryptsetup_rs::{CryptDevice, Luks1CryptDevice};
use libcryptsetup_sys::crypt_keyslot_info;
use structopt::clap::Shell;
use structopt::StructOpt;
use std::io::Write;
use std::process::exit;
use ctap::{FidoCredential, FidoErrorKind};
pub fn add_key_to_luks(
device: PathBuf,
secret: &[u8; 32],
old_secret: Box<dyn Fn() -> Fido2LuksResult<Vec<u8>>>,
exclusive: bool,
) -> Fido2LuksResult<u8> {
fn offer_format(
_dev: CryptDeviceOpenBuilder,
) -> Fido2LuksResult<CryptDeviceHandle<Luks1Params>> {
unimplemented!()
}
let dev =
|| -> luks::device::Result<CryptDeviceOpenBuilder> { luks::open(&device.canonicalize()?) };
use std::io::{Read, Write};
use std::str::FromStr;
use std::thread;
use std::time::Duration;
let prev_key = old_secret()?;
use std::borrow::Cow;
use std::collections::HashSet;
use std::fs::File;
use std::time::SystemTime;
let mut handle = match dev()?.luks1() {
Ok(handle) => handle,
Err(luks::device::Error::BlkidError(_)) => offer_format(dev()?)?,
Err(luks::device::Error::CryptsetupError(errno)) => {
//if i32::from(errno) == 555
dbg!(errno);
offer_format(dev()?)?
} //TODO: find correct errorno and offer to format as luks
err => err?,
};
handle.set_iteration_time(50);
let slot = handle.add_keyslot(secret, Some(prev_key.as_slice()), None)?;
if exclusive {
for old_slot in 0..8u8 {
if old_slot != slot
&& (handle.keyslot_status(old_slot.into()) == crypt_keyslot_info::CRYPT_SLOT_ACTIVE
|| handle.keyslot_status(old_slot.into())
== crypt_keyslot_info::CRYPT_SLOT_ACTIVE_LAST)
{
handle.destroy_keyslot(old_slot)?;
}
}
}
Ok(slot)
}
pub use cli_args::Args;
pub fn add_password_to_luks(
device: PathBuf,
secret: &[u8; 32],
new_secret: Box<dyn Fn() -> Fido2LuksResult<Vec<u8>>>,
add_password: bool,
) -> Fido2LuksResult<u8> {
let dev = luks::open(&device.canonicalize()?)?;
let mut handle = dev.luks1()?;
let prev_slot = if add_password {
Some(handle.add_keyslot(&secret[..], Some(&secret[..]), None)?)
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 {
None
};
let slot = handle.update_keyslot(&new_secret()?[..], &secret[..], prev_slot)?;
Ok(slot)
util::read_password("Authenticator PIN", false)
}
}
pub fn authenticator_connected() -> Fido2LuksResult<bool> {
Ok(!device::get_devices()?.is_empty())
}
fn derive_secret(
credentials: &[HexEncoded],
salt: &[u8; 32],
timeout: u64,
pin: Option<&str>,
) -> Fido2LuksResult<([u8; 32], FidoCredential)> {
let timeout = Duration::from_secs(timeout);
let start = SystemTime::now();
#[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(subcommand)]
pub command: Command,
}
#[derive(Debug, StructOpt, Clone)]
pub struct SecretGeneration {
/// FIDO credential id, generate using fido2luks credential
#[structopt(name = "credential-id", env = "FIDO2LUKS_CREDENTIAL_ID")]
pub credential_id: String,
/// Salt for secret generation, defaults to Password
#[structopt(name = "salt", env = "FIDO2LUKS_SALT", default_value = "Ask")]
pub salt: InputSalt,
/// Script used to obtain passwords, overridden by --interactive flag
#[structopt(
name = "password-helper",
env = "FIDO2LUKS_PASSWORD_HELPER",
default_value = "/usr/bin/systemd-ask-password 'Please enter second factor for LUKS disk encryption!'"
)]
pub password_helper: PasswordHelper,
}
impl SecretGeneration {
pub fn patch(&self, args: &Args) -> Self {
let mut me = self.clone();
if args.interactive {
me.password_helper = PasswordHelper::Stdin;
while let Ok(el) = start.elapsed() {
if el > timeout {
return Err(error::Fido2LuksError::NoAuthenticatorError);
}
me
if get_devices()
.map(|devices| !devices.is_empty())
.unwrap_or(false)
{
break;
}
thread::sleep(Duration::from_millis(500));
}
pub fn obtain_secret(&self) -> Fido2LuksResult<[u8; 32]> {
let salt = self.salt.obtain(&self.password_helper)?;
Ok(assemble_secret(
&perform_challenge(&self.credential_id, &salt)?,
&salt,
))
}
}
let credentials = credentials
.iter()
.map(|hex| FidoCredential {
id: hex.0.clone(),
public_key: None,
})
.collect::<Vec<_>>();
let credentials = credentials.iter().collect::<Vec<_>>();
let (unsalted, cred) =
perform_challenge(&credentials, salt, timeout - start.elapsed().unwrap(), pin)?;
#[derive(Debug, StructOpt)]
pub enum Command {
#[structopt(name = "print-secret")]
PrintSecret {
/// Prints the secret as binary instead of hex encoded
#[structopt(short = "b", long = "bin")]
binary: bool,
#[structopt(flatten)]
secret_gen: SecretGeneration,
},
/// Adds a generated key to the specified LUKS device
#[structopt(name = "add-key")]
AddKey {
#[structopt(env = "FIDO2LUKS_DEVICE")]
device: PathBuf,
/// Will wipe all other keys
#[structopt(short = "e", long = "exclusive")]
exclusive: bool,
/// Use a keyfile instead of a password
#[structopt(short = "d", long = "keyfile")]
keyfile: Option<PathBuf>,
#[structopt(flatten)]
secret_gen: SecretGeneration,
},
/// Replace a previously added key with a password
#[structopt(name = "replace-key")]
ReplaceKey {
#[structopt(env = "FIDO2LUKS_DEVICE")]
device: PathBuf,
/// Add the password and keep the key
#[structopt(short = "a", long = "add-password")]
add_password: bool,
/// Use a keyfile instead of a password
#[structopt(short = "d", long = "keyfile")]
keyfile: Option<PathBuf>,
#[structopt(flatten)]
secret_gen: SecretGeneration,
},
/// Open the LUKS device
#[structopt(name = "open")]
Open {
#[structopt(env = "FIDO2LUKS_DEVICE")]
device: PathBuf,
#[structopt(env = "FIDO2LUKS_MAPPER_NAME")]
name: String,
#[structopt(flatten)]
secret_gen: SecretGeneration,
},
/// Generate a new FIDO credential
#[structopt(name = "credential")]
Credential,
/// Check if an authenticator is connected
#[structopt(name = "connected")]
Connected,
Ok((sha256(&[salt, &unsalted[..]]), cred.clone()))
}
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 interactive = args.interactive;
match &args.command {
Command::Credential => {
let cred = make_credential_id()?;
Command::Credential {
authenticator,
name,
} => {
let pin_string;
let pin = if authenticator.pin {
pin_string = read_pin(authenticator)?;
Some(pin_string.as_ref())
} else {
None
};
let cred = make_credential_id(Some(name.as_ref()), pin)?;
println!("{}", hex::encode(&cred.id));
Ok(())
}
Command::PrintSecret {
binary,
ref secret_gen,
authenticator,
credentials,
secret,
} => {
let secret = secret_gen.patch(&args).obtain_secret()?;
if *binary {
stdout.write(&secret[..])?;
let pin_string;
let pin = if authenticator.pin {
pin_string = read_pin(authenticator)?;
Some(pin_string.as_ref())
} else {
stdout.write(hex::encode(&secret[..]).as_bytes())?;
None
};
let salt = if interactive || secret.password_helper == PasswordHelper::Stdin {
util::read_password_hashed("Password", false)
} else {
secret.salt.obtain_sha256(&secret.password_helper)
}?;
prompt_interaction(interactive);
let (secret, _cred) = derive_secret(
credentials.ids.0.as_slice(),
&salt,
authenticator.await_time,
pin,
)?;
if *binary {
stdout.write_all(&secret[..])?;
} else {
stdout.write_all(hex::encode(&secret[..]).as_bytes())?;
}
Ok(stdout.flush()?)
}
Command::AddKey {
device,
exclusive,
keyfile,
ref secret_gen,
} => {
let secret = secret_gen.patch(&args).obtain_secret()?;
let slot = add_key_to_luks(
device.clone(),
&secret,
if let Some(keyfile) = keyfile.clone() {
Box::new(move || util::read_keyfile(keyfile.clone()))
} else {
Box::new(|| {
util::read_password("Old password", true).map(|p| p.as_bytes().to_vec())
})
},
*exclusive,
)?;
println!(
"Added to key to device {}, slot: {}",
device.display(),
slot
);
Ok(())
luks,
authenticator,
credentials,
secret,
luks_mod,
existing_secret: other_secret,
token,
..
}
Command::ReplaceKey {
device,
add_password,
keyfile,
ref secret_gen,
| Command::ReplaceKey {
luks,
authenticator,
credentials,
secret,
luks_mod,
replacement: other_secret,
token,
..
} => {
let secret = secret_gen.patch(&args).obtain_secret()?;
let slot = add_password_to_luks(
device.clone(),
&secret,
if let Some(keyfile) = keyfile.clone() {
Box::new(move || util::read_keyfile(keyfile.clone()))
let pin = if authenticator.pin {
Some(read_pin(authenticator)?)
} 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 {
Box::new(|| {
util::read_password("Password to add", true).map(|p| p.as_bytes().to_vec())
})
},
*add_password,
)?;
println!(
"Added to password to device {}, slot: {}",
device.display(),
slot
);
Ok(())
secret.salt.obtain_sha256(&secret.password_helper)
}
};
let other_secret = |salt_q: &str,
verify: bool|
-> Fido2LuksResult<(Vec<u8>, Option<FidoCredential>)> {
match other_secret {
OtherSecret {
keyfile: Some(file),
..
} => Ok((util::read_keyfile(file)?, None)),
OtherSecret {
fido_device: true, ..
} => {
prompt_interaction(interactive);
Ok(derive_secret(
&credentials.ids.0,
&salt(salt_q, verify)?,
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(),
None,
)),
}
};
let secret = |q: &str, verify: bool| -> Fido2LuksResult<([u8; 32], FidoCredential)> {
prompt_interaction(interactive);
derive_secret(
&credentials.ids.0,
&salt(q, verify)?,
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)?;
let added_slot = luks_dev.add_key(
&new_secret,
&existing_secret[..],
luks_mod.kdf_time.or(Some(10)),
Some(&cred.id[..]).filter(|_| *token),
)?;
if *exclusive {
let destroyed = luks_dev.remove_keyslots(&[added_slot])?;
println!(
"Added to key to device {}, slot: {}\nRemoved {} old keys",
luks.device.display(),
added_slot,
destroyed
);
} else {
println!(
"Added to key to device {}, slot: {}",
luks.device.display(),
added_slot
);
}
Ok(())
}
Command::ReplaceKey { add_password, .. } => {
let (existing_secret, _) = secret("Current password", false)?;
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[..]),
)
} else {
luks_dev.replace_key(
&replacement_secret[..],
&existing_secret,
luks_mod.kdf_time,
cred.as_ref().filter(|_| *token).map(|cred| &cred.id[..]),
)
}?;
println!(
"Added to password to device {}, slot: {}",
luks.device.display(),
slot
);
Ok(())
}
_ => unreachable!(),
}
}
Command::Open {
device,
luks,
authenticator,
secret,
name,
ref secret_gen,
retries,
allow_discards,
..
}
| Command::OpenToken {
luks,
authenticator,
secret,
name,
retries,
allow_discards,
} => {
let secret = secret_gen.patch(&args).obtain_secret()?;
open_container(&device, &name, &secret)
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)
}
};
// Cow shouldn't be necessary
let secret = |credentials: Cow<'_, Vec<HexEncoded>>| {
prompt_interaction(interactive);
derive_secret(
credentials.as_ref(),
&salt("Password", false)?,
authenticator.await_time,
pin,
)
};
let mut retries = *retries;
let mut luks_dev = LuksDevice::load(&luks.device)?;
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(
&name,
Box::new(|credentials: Vec<String>| {
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)))
}),
luks.slot,
*allow_discards,
),
_ => unreachable!(),
};
match secret {
Err(e) => {
match e {
Fido2LuksError::WrongSecret if 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(|_| ()),
}
}
}
Command::Connected => match get_devices() {
Ok(ref devs) if !devs.is_empty() => {
@ -267,5 +342,141 @@ pub fn run_cli() -> Fido2LuksResult<()> {
}
_ => exit(1),
},
Command::Token(cmd) => match cmd {
TokenCommand::List {
device,
csv: dump_credentials,
} => {
let mut dev = LuksDevice::load(device)?;
let mut creds = Vec::new();
for token in dev.tokens()? {
let (id, token) = token?;
for cred in token.credential.iter() {
if !creds.contains(cred) {
creds.push(cred.clone());
if *dump_credentials {
print!("{}{}", if creds.len() == 1 { "" } else { "," }, cred);
}
}
}
if *dump_credentials {
continue;
}
println!(
"{}:\n\tSlots: {}\n\tCredentials: {}",
id,
if token.keyslots.is_empty() {
"None".into()
} else {
token.keyslots.iter().cloned().collect::<Vec<_>>().join(",")
},
token
.credential
.iter()
.map(|cred| format!(
"{} ({})",
cred,
creds.iter().position(|c| c == cred).unwrap().to_string()
))
.collect::<Vec<_>>()
.join(",")
);
}
if *dump_credentials {
println!();
}
Ok(())
}
TokenCommand::Add {
device,
credentials,
slot,
} => {
let mut dev = LuksDevice::load(device)?;
let mut tokens = Vec::new();
for token in dev.tokens()? {
let (id, token) = token?;
if token.keyslots.contains(&slot.to_string()) {
tokens.push((id, token));
}
}
let count = if tokens.is_empty() {
dev.add_token(&Fido2LuksToken::with_credentials(&credentials.ids.0, *slot))?;
1
} else {
tokens.len()
};
for (id, mut token) in tokens {
token
.credential
.extend(credentials.ids.0.iter().map(|h| h.to_string()));
dev.update_token(id, &token)?;
}
println!("Updated {} tokens", count);
Ok(())
}
TokenCommand::Remove {
device,
credentials,
token_id,
} => {
let mut dev = LuksDevice::load(device)?;
let mut tokens = Vec::new();
for token in dev.tokens()? {
let (id, token) = token?;
if let Some(token_id) = token_id {
if id == *token_id {
tokens.push((id, token));
}
} else {
tokens.push((id, token));
}
}
let count = tokens.len();
for (id, mut token) in tokens {
token.credential = token
.credential
.into_iter()
.filter(|cred| !credentials.ids.0.iter().any(|h| &h.to_string() == cred))
.collect();
dev.update_token(id, &token)?;
}
println!("Updated {} tokens", count);
Ok(())
}
TokenCommand::GC { device } => {
let mut dev = LuksDevice::load(device)?;
let mut creds: HashSet<String> = HashSet::new();
let mut remove = Vec::new();
for token in dev.tokens()? {
let (id, token) = token?;
if token.keyslots.is_empty() || token.credential.is_empty() {
creds.extend(token.credential);
remove.push(id);
}
}
for id in remove.iter().rev() {
dev.remove_token(*id)?;
}
println!(
"Removed {} tokens, affected credentials: {}",
remove.len(),
creds.into_iter().collect::<Vec<_>>().join(",")
);
Ok(())
}
},
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,
);
Ok(())
}
}
}

209
src/cli_args/config.rs Normal file
View File

@ -0,0 +1,209 @@
use crate::error::*;
use crate::*;
use ring::digest;
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
pub enum SecretInput {
AskPassword,
String(String),
File { path: PathBuf },
}
impl Default for SecretInput {
fn default() -> Self {
SecretInput::AskPassword
}
}
impl From<&str> for SecretInput {
fn from(s: &str) -> Self {
let mut parts = s.split(':');
match parts.next() {
Some("ask") | Some("Ask") => SecretInput::AskPassword,
Some("file") => SecretInput::File {
path: parts.collect::<Vec<_>>().join(":").into(),
},
Some("string") => SecretInput::String(parts.collect::<Vec<_>>().join(":")),
_ => Self::default(),
}
}
}
impl FromStr for SecretInput {
type Err = Fido2LuksError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(s))
}
}
impl fmt::Display for SecretInput {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
f.write_str(&match self {
SecretInput::AskPassword => "ask".to_string(),
SecretInput::String(s) => ["string", s].join(":"),
SecretInput::File { path } => ["file", path.display().to_string().as_str()].join(":"),
})
}
}
impl SecretInput {
pub fn obtain_string(&self, password_helper: &PasswordHelper) -> Fido2LuksResult<String> {
Ok(String::from_utf8(self.obtain(password_helper)?)?)
}
pub fn obtain(&self, password_helper: &PasswordHelper) -> Fido2LuksResult<Vec<u8>> {
let mut secret = Vec::new();
match self {
SecretInput::File { path } => {
//TODO: replace with try_blocks
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::String(s) => secret.extend_from_slice(s.as_bytes()),
}
Ok(secret)
}
pub fn obtain_sha256(&self, password_helper: &PasswordHelper) -> Fido2LuksResult<[u8; 32]> {
let mut digest = digest::Context::new(&digest::SHA256);
match self {
SecretInput::File { path } => {
let mut do_io = || {
let mut reader = File::open(path)?;
let mut buf = [0u8; 512];
loop {
let red = reader.read(&mut buf)?;
digest.update(&buf[0..red]);
if red == 0 {
break;
}
}
Ok(())
};
do_io().map_err(|cause| Fido2LuksError::KeyfileError { cause })?;
}
_ => digest.update(self.obtain(password_helper)?.as_slice()),
}
let mut salt = [0u8; 32];
salt.as_mut().copy_from_slice(digest.finish().as_ref());
Ok(salt)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PasswordHelper {
Script(String),
#[allow(dead_code)]
Systemd,
Stdin,
}
impl Default for PasswordHelper {
fn default() -> Self {
PasswordHelper::Script(
"/usr/bin/env systemd-ask-password 'Please enter second factor for LUKS disk encryption!'"
.into(),
)
}
}
impl From<&str> for PasswordHelper {
fn from(s: &str) -> Self {
match s {
"stdin" => PasswordHelper::Stdin,
s => PasswordHelper::Script(s.into()),
}
}
}
impl FromStr for PasswordHelper {
type Err = Fido2LuksError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(s))
}
}
impl fmt::Display for PasswordHelper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
f.write_str(&match self {
PasswordHelper::Stdin => "stdin".to_string(),
PasswordHelper::Systemd => "systemd".to_string(),
PasswordHelper::Script(path) => path.clone(),
})
}
}
impl PasswordHelper {
pub fn obtain(&self) -> Fido2LuksResult<String> {
use PasswordHelper::*;
match self {
Systemd => unimplemented!(),
Stdin => Ok(util::read_password("Password", true)?),
Script(password_helper) => {
let password = Command::new("sh")
.arg("-c")
.arg(&password_helper)
.output()
.map_err(|e| Fido2LuksError::AskPassError {
cause: error::AskPassError::IO(e),
})?
.stdout;
Ok(String::from_utf8(password)?.trim().to_owned())
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn input_salt_from_str() {
assert_eq!(
"file:/tmp/abc".parse::<SecretInput>().unwrap(),
SecretInput::File {
path: "/tmp/abc".into()
}
);
assert_eq!(
"string:abc".parse::<SecretInput>().unwrap(),
SecretInput::String("abc".into())
);
assert_eq!(
"ask".parse::<SecretInput>().unwrap(),
SecretInput::AskPassword
);
assert_eq!(
"lol".parse::<SecretInput>().unwrap(),
SecretInput::default()
);
}
#[test]
fn input_salt_obtain() {
assert_eq!(
SecretInput::String("abc".into())
.obtain_sha256(&PasswordHelper::Stdin)
.unwrap(),
[
186, 120, 22, 191, 143, 1, 207, 234, 65, 65, 64, 222, 93, 174, 34, 35, 176, 3, 97,
163, 150, 23, 122, 156, 180, 16, 255, 97, 242, 0, 21, 173
]
)
}
}

299
src/cli_args/mod.rs Normal file
View File

@ -0,0 +1,299 @@
use std::fmt::{Display, Error, Formatter};
use std::path::PathBuf;
use std::str::FromStr;
use structopt::clap::AppSettings;
use structopt::StructOpt;
mod config;
pub use config::*;
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct HexEncoded(pub Vec<u8>);
impl Display for HexEncoded {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
f.write_str(&hex::encode(&self.0))
}
}
impl AsRef<[u8]> for HexEncoded {
fn as_ref(&self) -> &[u8] {
&self.0[..]
}
}
impl FromStr for HexEncoded {
type Err = hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(HexEncoded(hex::decode(s)?))
}
}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct CommaSeparated<T: FromStr + Display>(pub Vec<T>);
impl<T: Display + FromStr> Display for CommaSeparated<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
for i in &self.0 {
f.write_str(&i.to_string())?;
f.write_str(",")?;
}
Ok(())
}
}
impl<T: Display + FromStr> FromStr for CommaSeparated<T> {
type Err = <T as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(CommaSeparated(
s.split(',')
.map(|part| <T as FromStr>::from_str(part))
.collect::<Result<Vec<_>, _>>()?,
))
}
}
#[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>,
}
#[derive(Debug, StructOpt)]
pub struct AuthenticatorParameters {
/// Request a PIN to unlock the authenticator
#[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>,
/// Await for an authenticator to be connected, timeout after n seconds
#[structopt(
long = "await-dev",
name = "await-dev",
env = "FIDO2LUKS_DEVICE_AWAIT",
default_value = "15"
)]
pub await_time: u64,
}
#[derive(Debug, StructOpt)]
pub struct LuksParameters {
#[structopt(env = "FIDO2LUKS_DEVICE")]
pub device: PathBuf,
/// Try to unlock the device using a specifc keyslot, ignore all other slots
#[structopt(long = "slot", env = "FIDO2LUKS_DEVICE_SLOT")]
pub slot: Option<u32>,
}
#[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")]
pub kdf_time: Option<u64>,
}
#[derive(Debug, StructOpt)]
pub struct SecretParameters {
/// Salt for secret generation, defaults to 'ask'
///
/// Options:{n}
/// - ask : Prompt user using password helper{n}
/// - file:<PATH> : Will read <FILE>{n}
/// - string:<STRING> : Will use <STRING>, which will be handled like a password provided to the 'ask' option{n}
#[structopt(
name = "salt",
long = "salt",
env = "FIDO2LUKS_SALT",
default_value = "ask"
)]
pub salt: SecretInput,
/// Script used to obtain passwords, overridden by --interactive flag
#[structopt(
name = "password-helper",
env = "FIDO2LUKS_PASSWORD_HELPER",
default_value = "/usr/bin/env systemd-ask-password 'Please enter second factor for LUKS disk encryption!'"
)]
pub password_helper: 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(subcommand)]
pub command: Command,
}
#[derive(Debug, StructOpt, Clone)]
pub struct OtherSecret {
/// Use a keyfile instead of a password
#[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
#[structopt(short = "f", long = "fido-device", conflicts_with = "keyfile")]
pub fido_device: bool,
}
#[derive(Debug, StructOpt)]
pub enum Command {
#[structopt(name = "print-secret")]
PrintSecret {
/// Prints the secret as binary instead of hex encoded
#[structopt(short = "b", long = "bin")]
binary: bool,
#[structopt(flatten)]
credentials: Credentials,
#[structopt(flatten)]
authenticator: AuthenticatorParameters,
#[structopt(flatten)]
secret: SecretParameters,
},
/// Adds a generated key to the specified LUKS device
#[structopt(name = "add-key")]
AddKey {
#[structopt(flatten)]
luks: LuksParameters,
#[structopt(flatten)]
credentials: Credentials,
#[structopt(flatten)]
authenticator: AuthenticatorParameters,
#[structopt(flatten)]
secret: SecretParameters,
/// 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,
#[structopt(flatten)]
existing_secret: OtherSecret,
#[structopt(flatten)]
luks_mod: LuksModParameters,
},
/// Replace a previously added key with a password
#[structopt(name = "replace-key")]
ReplaceKey {
#[structopt(flatten)]
luks: LuksParameters,
#[structopt(flatten)]
credentials: Credentials,
#[structopt(flatten)]
authenticator: AuthenticatorParameters,
#[structopt(flatten)]
secret: SecretParameters,
/// 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,
#[structopt(flatten)]
replacement: OtherSecret,
#[structopt(flatten)]
luks_mod: LuksModParameters,
},
/// Open the LUKS device
#[structopt(name = "open")]
Open {
#[structopt(flatten)]
luks: LuksParameters,
#[structopt(env = "FIDO2LUKS_MAPPER_NAME")]
name: String,
#[structopt(flatten)]
credentials: Credentials,
#[structopt(flatten)]
authenticator: AuthenticatorParameters,
#[structopt(flatten)]
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,
/// Pass SSD trim instructions to the underlying block device
#[structopt(long = "allow-discards")]
allow_discards: bool,
},
/// Generate a new FIDO credential
#[structopt(name = "credential")]
Credential {
#[structopt(flatten)]
authenticator: AuthenticatorParameters,
/// 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
#[structopt(name = "completions", setting = AppSettings::Hidden)]
GenerateCompletions {
/// Shell to generate completions for: bash, fish
#[structopt(possible_values = &["bash", "fish"])]
shell: String,
out_dir: PathBuf,
},
}
///LUKS2 token related operations
#[derive(Debug, StructOpt)]
pub enum TokenCommand {
/// List all tokens associated with the specified device
List {
#[structopt(env = "FIDO2LUKS_DEVICE")]
device: PathBuf,
/// Dump all credentials as CSV
#[structopt(long = "csv")]
csv: bool,
},
/// Add credential to a keyslot
Add {
#[structopt(env = "FIDO2LUKS_DEVICE")]
device: PathBuf,
#[structopt(flatten)]
credentials: Credentials,
/// Slot to which the credentials will be added
#[structopt(long = "slot", env = "FIDO2LUKS_DEVICE_SLOT")]
slot: u32,
},
/// Remove credentials from token(s)
Remove {
#[structopt(env = "FIDO2LUKS_DEVICE")]
device: PathBuf,
#[structopt(flatten)]
credentials: Credentials,
/// Token from which the credentials will be removed
#[structopt(long = "token")]
token_id: Option<u32>,
},
/// Remove all unassigned tokens
GC {
#[structopt(env = "FIDO2LUKS_DEVICE")]
device: PathBuf,
},
}

View File

@ -1,150 +0,0 @@
use crate::error::*;
use crate::*;
use crypto::digest::Digest;
use crypto::sha2::Sha256;
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
#[derive(Debug, Clone)]
pub enum InputSalt {
AskPassword,
File { path: PathBuf },
Both { path: PathBuf },
}
impl Default for InputSalt {
fn default() -> Self {
InputSalt::AskPassword
}
}
impl From<&str> for InputSalt {
fn from(s: &str) -> Self {
if PathBuf::from(s).exists() && s != "Ask" {
InputSalt::File { path: s.into() }
} else {
InputSalt::AskPassword
}
}
}
impl FromStr for InputSalt {
type Err = Fido2LuksError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(s))
}
}
impl fmt::Display for InputSalt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
f.write_str(&match self {
InputSalt::AskPassword => "ask".to_string(),
InputSalt::File { path } => path.display().to_string(),
InputSalt::Both { path } => ["ask", path.display().to_string().as_str()].join(" + "),
})
}
}
impl InputSalt {
pub fn obtain(&self, password_helper: &PasswordHelper) -> Fido2LuksResult<[u8; 32]> {
let mut digest = Sha256::new();
match self {
InputSalt::File { path } => {
let mut do_io = || {
let mut reader = File::open(path)?;
let mut buf = [0u8; 512];
loop {
let red = reader.read(&mut buf)?;
digest.input(&buf[0..red]);
if red == 0 {
break;
}
}
Ok(())
};
do_io().map_err(|cause| Fido2LuksError::KeyfileError { cause })?;
}
InputSalt::AskPassword => {
digest.input(password_helper.obtain()?.as_bytes());
}
InputSalt::Both { path } => {
digest.input(&InputSalt::AskPassword.obtain(password_helper)?);
digest.input(&InputSalt::File { path: path.clone() }.obtain(password_helper)?)
}
}
let mut salt = [0u8; 32];
digest.result(&mut salt);
Ok(salt)
}
}
#[derive(Debug, Clone)]
pub enum PasswordHelper {
Script(String),
Systemd,
Stdin,
}
impl Default for PasswordHelper {
fn default() -> Self {
PasswordHelper::Script(
"/usr/bin/systemd-ask-password 'Please enter second factor for LUKS disk encryption!'"
.into(),
)
}
}
impl From<&str> for PasswordHelper {
fn from(s: &str) -> Self {
match s {
"stdin" => PasswordHelper::Stdin,
s => PasswordHelper::Script(s.into()),
}
}
}
impl FromStr for PasswordHelper {
type Err = Fido2LuksError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(s))
}
}
impl fmt::Display for PasswordHelper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
f.write_str(&match self {
PasswordHelper::Stdin => "stdin".to_string(),
PasswordHelper::Systemd => "systemd".to_string(),
PasswordHelper::Script(path) => path.clone(),
})
}
}
impl PasswordHelper {
pub fn obtain(&self) -> Fido2LuksResult<String> {
use PasswordHelper::*;
match self {
Systemd => unimplemented!(),
Stdin => Ok(util::read_password("Password", true)?),
Script(password_helper) => {
let mut helper_parts = password_helper.split(" ");
let password = Command::new((&mut helper_parts).next().unwrap())
.args(helper_parts)
.output()
.map_err(|e| Fido2LuksError::AskPassError {
cause: error::AskPassError::IO(e),
})?
.stdout;
Ok(String::from_utf8(password)?.trim().to_owned())
}
}
}
}

View File

@ -1,51 +1,61 @@
use crate::error::*;
use ctap;
use ctap::extensions::hmac::{FidoHmacCredential, HmacExtension};
use ctap::{FidoDevice, FidoError, FidoErrorKind};
use crate::util;
use ctap::{
self, extensions::hmac::HmacExtension, request_multiple_devices, FidoAssertionRequestBuilder,
FidoCredential, FidoCredentialRequestBuilder, FidoDevice, FidoError, FidoErrorKind,
};
use std::time::Duration;
pub fn make_credential_id() -> Fido2LuksResult<FidoHmacCredential> {
let mut errs = Vec::new();
match get_devices()? {
ref devs if devs.is_empty() => Err(Fido2LuksError::NoAuthenticatorError)?,
devs => {
for mut dev in devs.into_iter() {
match dev.make_hmac_credential() {
Ok(cred) => {
return Ok(cred);
}
Err(e) => {
errs.push(e);
}
}
}
}
const RP_ID: &str = "fido2luks";
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);
}
Err(errs.pop().ok_or(Fido2LuksError::NoAuthenticatorError)?)?
let request = request.build().unwrap();
let make_credential = |device: &mut FidoDevice| {
if let Some(pin) = pin {
device.unlock(pin)?;
}
device.make_hmac_credential(&request)
};
Ok(request_multiple_devices(
get_devices()?
.iter_mut()
.map(|device| (device, &make_credential)),
None,
)?)
}
pub fn perform_challenge(credential_id: &str, salt: &[u8; 32]) -> Fido2LuksResult<[u8; 32]> {
let cred = FidoHmacCredential {
id: hex::decode(credential_id).unwrap(),
rp_id: "hmac".to_string(),
};
let mut errs = Vec::new();
match get_devices()? {
ref devs if devs.is_empty() => Err(Fido2LuksError::NoAuthenticatorError)?,
devs => {
for mut dev in devs.into_iter() {
match dev.hmac_challange(&cred, &salt[..]) {
Ok(secret) => {
return Ok(secret);
}
Err(e) => {
errs.push(e);
}
}
}
pub fn perform_challenge<'a>(
credentials: &'a [&'a FidoCredential],
salt: &[u8; 32],
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)?;
}
}
Err(errs.pop().ok_or(Fido2LuksError::NoAuthenticatorError)?)?
device.get_hmac_assertion(&request, &util::sha256(&[&salt[..]]), None)
};
let (credential, (secret, _)) = request_multiple_devices(
get_devices()?
.iter_mut()
.map(|device| (device, &get_assertion)),
Some(timeout),
)?;
Ok((secret, credential))
}
pub fn get_devices() -> Fido2LuksResult<Vec<FidoDevice>> {
@ -54,7 +64,7 @@ pub fn get_devices() -> Fido2LuksResult<Vec<FidoDevice>> {
match FidoDevice::new(&di) {
Err(e) => match e.kind() {
FidoErrorKind::ParseCtap | FidoErrorKind::DeviceUnsupported => (),
err => Err(FidoError::from(err))?,
err => return Err(FidoError::from(err).into()),
},
Ok(dev) => devices.push(dev),
}

View File

@ -1,5 +1,9 @@
use ctap::FidoError;
use libcryptsetup_rs::LibcryptErr;
use std::io;
use std::io::ErrorKind;
use std::string::FromUtf8Error;
use Fido2LuksError::*;
pub type Fido2LuksResult<T> = Result<T, Fido2LuksError>;
@ -11,11 +15,15 @@ pub enum Fido2LuksError {
KeyfileError { cause: io::Error },
#[fail(display = "authenticator error: {}", cause)]
AuthenticatorError { cause: ctap::FidoError },
#[fail(display = "no authenticator found, please ensure you device is plugged in")]
#[fail(display = "no authenticator found, please ensure your device is plugged in")]
NoAuthenticatorError,
#[fail(display = "luks err")]
LuksError { cause: cryptsetup_rs::device::Error },
#[fail(display = "no authenticator found, please ensure you device is plugged in")]
#[fail(display = " {}", cause)]
CryptsetupError {
cause: libcryptsetup_rs::LibcryptErr,
},
#[fail(display = "{}", cause)]
LuksError { cause: LuksError },
#[fail(display = "{}", cause)]
IoError { cause: io::Error },
#[fail(display = "supplied secret isn't valid for this device")]
WrongSecret,
@ -23,6 +31,19 @@ pub enum Fido2LuksError {
StringEncodingError { cause: FromUtf8Error },
}
impl Fido2LuksError {
pub fn exit_code(&self) -> i32 {
use Fido2LuksError::*;
match self {
AskPassError { .. } | KeyfileError { .. } => 2,
AuthenticatorError { .. } => 3,
NoAuthenticatorError => 4,
WrongSecret => 5,
_ => 1,
}
}
}
#[derive(Debug, Fail)]
pub enum AskPassError {
#[fail(display = "unable to retrieve password: {}", _0)]
@ -31,8 +52,38 @@ pub enum AskPassError {
Mismatch,
}
use std::string::FromUtf8Error;
use Fido2LuksError::*;
#[derive(Debug, Fail)]
pub enum LuksError {
#[fail(display = "This feature requires to the LUKS device to be formatted as LUKS 2")]
Luks2Required,
#[fail(display = "Invalid token: {}", _0)]
InvalidToken(String),
#[fail(display = "No token found")]
NoToken,
#[fail(display = "The device already exists")]
DeviceExists,
}
impl LuksError {
pub fn activate(e: LibcryptErr) -> Fido2LuksError {
match e {
LibcryptErr::IOError(ref io) => match io.raw_os_error() {
Some(1) if io.kind() == ErrorKind::PermissionDenied => Fido2LuksError::WrongSecret,
Some(17) => Fido2LuksError::LuksError {
cause: LuksError::DeviceExists,
},
_ => return Fido2LuksError::CryptsetupError { cause: e },
},
_ => Fido2LuksError::CryptsetupError { cause: e },
}
}
}
impl From<LuksError> for Fido2LuksError {
fn from(e: LuksError) -> Self {
Fido2LuksError::LuksError { cause: e }
}
}
impl From<FidoError> for Fido2LuksError {
fn from(e: FidoError) -> Self {
@ -40,17 +91,16 @@ impl From<FidoError> for Fido2LuksError {
}
}
impl From<cryptsetup_rs::device::Error> for Fido2LuksError {
fn from(e: cryptsetup_rs::device::Error) -> Self {
impl From<LibcryptErr> for Fido2LuksError {
fn from(e: LibcryptErr) -> Self {
match e {
cryptsetup_rs::device::Error::CryptsetupError(error_no) if error_no.0 == 1i32 => {
LibcryptErr::IOError(e) if e.raw_os_error().iter().any(|code| code == &1i32) => {
WrongSecret
}
e => LuksError { cause: e },
_ => CryptsetupError { cause: e },
}
}
}
impl From<io::Error> for Fido2LuksError {
fn from(e: io::Error) -> Self {
IoError { cause: e }

333
src/luks.rs Normal file
View File

@ -0,0 +1,333 @@
use crate::error::*;
use libcryptsetup_rs::{
CryptActivateFlag, CryptActivateFlags, CryptDevice, CryptInit, CryptTokenInfo,
EncryptionFormat, KeyslotInfo, TokenInput,
};
use std::collections::{HashMap, HashSet};
use std::path::Path;
pub struct LuksDevice {
device: CryptDevice,
luks2: Option<bool>,
}
impl LuksDevice {
pub fn load<P: AsRef<Path>>(path: P) -> Fido2LuksResult<LuksDevice> {
let mut device = CryptInit::init(path.as_ref())?;
device.context_handle().load::<()>(None, None)?;
Ok(Self {
device,
luks2: None,
})
}
pub fn is_luks2(&mut self) -> Fido2LuksResult<bool> {
if let Some(luks2) = self.luks2 {
Ok(luks2)
} else {
self.luks2 = Some(match self.device.format_handle().get_type()? {
EncryptionFormat::Luks2 => true,
_ => false,
});
self.is_luks2()
}
}
fn require_luks2(&mut self) -> Fido2LuksResult<()> {
if !self.is_luks2()? {
return Err(LuksError::Luks2Required.into());
}
Ok(())
}
pub fn tokens<'a>(
&'a mut self,
) -> Fido2LuksResult<Box<dyn Iterator<Item = Fido2LuksResult<(u32, Fido2LuksToken)>> + 'a>>
{
self.require_luks2()?;
Ok(Box::new(
(0..32)
.map(move |i| {
let status = match self.device.token_handle().status(i) {
Ok(status) => status,
Err(err) => return Some(Err(Fido2LuksError::from(err))),
};
match status {
CryptTokenInfo::Inactive => return None,
CryptTokenInfo::Internal(s)
| CryptTokenInfo::InternalUnknown(s)
| CryptTokenInfo::ExternalUnknown(s)
| CryptTokenInfo::External(s)
if &s != Fido2LuksToken::default_type() =>
{
return None
}
_ => (),
};
let json = match self.device.token_handle().json_get(i) {
Ok(json) => json,
Err(err) => return Some(Err(Fido2LuksError::from(err))),
};
let info: Fido2LuksToken =
match serde_json::from_value(json.clone()).map_err(|_| {
Fido2LuksError::LuksError {
cause: LuksError::InvalidToken(json.to_string()),
}
}) {
Ok(info) => info,
Err(err) => return Some(Err(Fido2LuksError::from(err))),
};
Some(Ok((i, info)))
})
.filter_map(|o| o),
))
}
pub fn find_token(&mut self, slot: u32) -> Fido2LuksResult<Option<(u32, Fido2LuksToken)>> {
let slot_str = slot.to_string();
for token in self.tokens()? {
let (id, token) = token?;
if token.keyslots.contains(&slot_str) {
return Ok(Some((id, token)));
}
}
Ok(None)
}
pub fn add_token(&mut self, data: &Fido2LuksToken) -> Fido2LuksResult<()> {
self.require_luks2()?;
self.device
.token_handle()
.json_set(TokenInput::AddToken(&serde_json::to_value(&data).unwrap()))?;
Ok(())
}
pub fn remove_token(&mut self, token: u32) -> Fido2LuksResult<()> {
self.require_luks2()?;
self.device
.token_handle()
.json_set(TokenInput::RemoveToken(token))?;
Ok(())
}
pub fn update_token(&mut self, token: u32, data: &Fido2LuksToken) -> Fido2LuksResult<()> {
self.require_luks2()?;
self.device
.token_handle()
.json_set(TokenInput::ReplaceToken(
token,
&serde_json::to_value(&data).unwrap(),
))?;
Ok(())
}
pub fn add_key(
&mut self,
secret: &[u8],
old_secret: &[u8],
iteration_time: Option<u64>,
credential_id: Option<&[u8]>,
) -> Fido2LuksResult<u32> {
if let Some(millis) = iteration_time {
self.device.settings_handle().set_iteration_time(millis)
}
let slot = self
.device
.keyslot_handle()
.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(),
))?;
}
Ok(slot)
}
pub fn remove_keyslots(&mut self, exclude: &[u32]) -> Fido2LuksResult<u32> {
let mut destroyed = 0;
let mut tokens = Vec::new();
for slot in 0..256 {
match self.device.keyslot_handle().status(slot)? {
KeyslotInfo::Inactive => continue,
KeyslotInfo::Active | KeyslotInfo::ActiveLast if !exclude.contains(&slot) => {
if self.is_luks2()? {
if let Some((id, _token)) = self.find_token(slot)? {
tokens.push(id);
}
}
self.device.keyslot_handle().destroy(slot)?;
destroyed += 1;
}
KeyslotInfo::ActiveLast => break,
_ => (),
}
if self.device.keyslot_handle().status(slot)? == KeyslotInfo::ActiveLast {
break;
}
}
// Ensure indices stay valid
tokens.sort();
for token in tokens.iter().rev() {
self.remove_token(*token)?;
}
Ok(destroyed)
}
pub fn replace_key(
&mut self,
secret: &[u8],
old_secret: &[u8],
iteration_time: Option<u64>,
credential_id: Option<&[u8]>,
) -> Fido2LuksResult<u32> {
if let Some(millis) = iteration_time {
self.device.settings_handle().set_iteration_time(millis)
}
// Use activate dry-run to locate keyslot
let slot = self.device.activate_handle().activate_by_passphrase(
None,
None,
old_secret,
CryptActivateFlags::empty(),
)?;
self.device.keyslot_handle().change_by_passphrase(
Some(slot),
Some(slot),
old_secret,
secret,
)? 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 {
self.device
.token_handle()
.json_set(TokenInput::ReplaceToken(token, &json))?;
} else {
self.device
.token_handle()
.json_set(TokenInput::AddToken(&json))?;
}
}
}
Ok(slot)
}
pub fn activate(
&mut self,
name: &str,
secret: &[u8],
slot_hint: Option<u32>,
allow_discard: bool,
) -> Fido2LuksResult<u32> {
let mut flags = CryptActivateFlags::empty();
if allow_discard {
flags = CryptActivateFlags::new(vec![CryptActivateFlag::AllowDiscards]);
}
self.device
.activate_handle()
.activate_by_passphrase(Some(name), slot_hint, secret, flags)
.map_err(LuksError::activate)
}
pub fn activate_token(
&mut self,
name: &str,
secret: impl Fn(Vec<String>) -> Fido2LuksResult<([u8; 32], String)>,
slot_hint: Option<u32>,
allow_discard: bool,
) -> Fido2LuksResult<u32> {
if !self.is_luks2()? {
return Err(LuksError::Luks2Required.into());
}
let mut creds: HashMap<String, HashSet<u32>> = HashMap::new();
for token in self.tokens()? {
let token = match token {
Ok((_id, t)) => t,
_ => continue, // An corrupted token should't lock the user out
};
let slots = || {
token
.keyslots
.iter()
.filter_map(|slot| slot.parse::<u32>().ok())
};
for cred in token.credential.iter() {
creds
.entry(cred.clone())
.or_insert_with(|| slots().collect::<HashSet<u32>>())
.extend(slots());
}
}
if creds.is_empty() {
return Err(Fido2LuksError::LuksError {
cause: LuksError::NoToken,
});
}
let (secret, credential) = secret(creds.keys().cloned().collect())?;
let empty;
let slots = if let Some(slots) = creds.get(&credential) {
slots
} else {
empty = HashSet::new();
&empty
};
//Try slots associated with the credential used
let slots = slots.iter().cloned().map(Option::Some).chain(
std::iter::once(slot_hint) // Try slot hint if there is one
.take(slot_hint.is_some() as usize)
.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) {
Err(Fido2LuksError::WrongSecret) => (),
res => return res,
}
}
Err(Fido2LuksError::WrongSecret)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fido2LuksToken {
#[serde(rename = "type")]
pub type_: String,
pub credential: HashSet<String>,
pub keyslots: HashSet<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 with_credentials<I: IntoIterator<Item = B>, B: AsRef<[u8]>>(
credentials: I,
slot: u32,
) -> Self {
Self {
credential: credentials
.into_iter()
.map(|cred| hex::encode(cred.as_ref()))
.collect(),
keyslots: vec![slot.to_string()].into_iter().collect(),
..Default::default()
}
}
pub fn default_type() -> &'static str {
"fido2luks"
}
}
impl Default for Fido2LuksToken {
fn default() -> Self {
Self {
type_: Self::default_type().into(),
credential: HashSet::new(),
keyslots: HashSet::new(),
}
}
}

View File

@ -1,38 +1,27 @@
#[macro_use]
extern crate failure;
extern crate ctap_hmac as ctap;
#[macro_use]
extern crate serde_derive;
use crate::cli::*;
use crate::config::*;
use crate::device::*;
use crate::error::*;
use crypto::digest::Digest;
use crypto::sha2::Sha256;
use cryptsetup_rs as luks;
use cryptsetup_rs::Luks1CryptDevice;
use std::io::{self};
use std::path::PathBuf;
use std::io;
use std::process::exit;
mod cli;
mod config;
pub mod cli_args;
mod device;
mod error;
mod luks;
mod util;
fn open_container(device: &PathBuf, name: &str, secret: &[u8; 32]) -> Fido2LuksResult<()> {
let mut handle = luks::open(device.canonicalize()?)?.luks1()?;
let _slot = handle.activate(name, &secret[..])?;
Ok(())
}
fn assemble_secret(hmac_result: &[u8], salt: &[u8]) -> [u8; 32] {
let mut digest = Sha256::new();
digest.input(salt);
digest.input(hmac_result);
let mut secret = [0u8; 32];
digest.result(&mut secret);
secret
}
fn main() -> Fido2LuksResult<()> {
run_cli()
match run_cli() {
Err(e) => {
eprintln!("{:?}", e);
exit(e.exit_code())
}
_ => exit(0),
}
}

View File

@ -1,8 +1,19 @@
use crate::error::*;
use ring::digest;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
pub fn sha256(messages: &[&[u8]]) -> [u8; 32] {
let mut digest = digest::Context::new(&digest::SHA256);
for m in messages.iter() {
digest.update(m);
}
let mut secret = [0u8; 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("")))? {
ref pass
@ -12,12 +23,16 @@ pub fn read_password(q: &str, verify: bool) -> Fido2LuksResult<String> {
{
Err(Fido2LuksError::AskPassError {
cause: AskPassError::Mismatch,
})?
})
}
pass => Ok(pass),
}
}
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();