diff --git a/.env b/.env index b04b632..0f73893 100644 --- a/.env +++ b/.env @@ -40,6 +40,14 @@ ## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs # DOMAIN=https://bw.domain.tld:8443 +## Yubico (Yubikey) Settings +## Set your Client ID and Secret Key for Yubikey OTP +## You can generate it here: https://upgrade.yubico.com/getapikey/ +## You can optionally specify a custom OTP server +# YUBICO_CLIENT_ID=11111 +# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA +# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify + ## Rocket specific settings, check Rocket documentation to learn more # ROCKET_ENV=staging # ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app diff --git a/Cargo.lock b/Cargo.lock index 0d29f26..7233b0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,14 @@ dependencies = [ "safemem 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "base64" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "bitflags" version = "1.0.4" @@ -119,6 +127,7 @@ dependencies = [ "u2f 0.1.2 (git+https://github.com/wisespace-io/u2f-rs?rev=193de35093a44)", "uuid 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "ws 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)", + "yubico 0.4.0 (git+https://github.com/dani-garcia/yubico-rs)", ] [[package]] @@ -1348,6 +1357,16 @@ dependencies = [ "scheduled-thread-pool 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand" version = "0.4.3" @@ -1370,6 +1389,33 @@ dependencies = [ "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_isaac 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_pcg 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_xorshift 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_chacha" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rand_core" version = "0.2.2" @@ -1383,6 +1429,39 @@ name = "rand_core" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_isaac" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_pcg" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rayon" version = "0.7.1" @@ -1559,6 +1638,18 @@ dependencies = [ "serde_json 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rust-crypto" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gcc 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustc-demangle" version = "0.1.9" @@ -1872,6 +1963,14 @@ dependencies = [ "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "threadpool" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "time" version = "0.1.40" @@ -2319,6 +2418,20 @@ name = "yansi" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "yubico" +version = "0.4.0" +source = "git+https://github.com/dani-garcia/yubico-rs#f93a88834e4ebaaed0169850ff1a5e32a7d01719" +dependencies = [ + "base64 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)", + "threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c" "checksum aho-corasick 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "68f56c7353e5a9547cbd76ed90f7bb5ffc3ba09d4ea9bd1d8c06c8b1142eeb5a" @@ -2328,6 +2441,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum ascii_utils 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" "checksum backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "89a47830402e9981c5c41223151efcced65a0510c13097c769cede7efb34782a" "checksum backtrace-sys 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)" = "c66d56ac8dabd07f6aacdaf633f4b8262f5b3601a810a0dcddffd5c22c69daa0" +"checksum base64 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "621fc7ecb8008f86d7fb9b95356cd692ce9514b80a86d85b397f32a22da7b9e2" "checksum base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557" "checksum base64 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "96434f987501f0ed4eb336a411e0631ecd1afa11574fe148587adc4ff96143c9" "checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" @@ -2470,10 +2584,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8" "checksum quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "dd636425967c33af890042c483632d33fa7a18f19ad1d7ea72e8998c6ef8dea5" "checksum r2d2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f9078ca6a8a5568ed142083bb2f7dc9295b69d16f867ddcc9849e51b17d8db46" +"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" "checksum rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd" "checksum rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e464cd887e869cddcae8792a4ee31d23c7edd516700695608f5b98c67ee0131c" +"checksum rand 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de3f08319b5395bd19b70e73c4c465329495db02dafeb8ca711a20f1c2bd058c" +"checksum rand_chacha 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "771b009e3a508cb67e8823dda454aaa5368c7bc1c16829fb77d3e980440dd34a" "checksum rand_core 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1961a422c4d189dfb50ffa9320bf1f2a9bd54ecb92792fb9477f99a1045f3372" "checksum rand_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0905b6b7079ec73b314d4c748701f6931eb79fd97c668caa3f1899b22b32c6db" +"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +"checksum rand_isaac 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d6ecfe9ebf36acd47a49d150990b047a5f7db0a7236ee2414b7ff5cc1097c7b" +"checksum rand_pcg 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "086bd09a33c7044e56bb44d5bdde5a60e7f119a9e95b0775f545de759a32fe05" +"checksum rand_xorshift 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effa3fcaa47e18db002bdde6060944b6d2f9cfd8db471c30e873448ad9187be3" "checksum rayon 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a77c51c07654ddd93f6cb543c7a849863b03abc7e82591afda6dc8ad4ac3ac4a" "checksum rayon-core 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b055d1e92aba6877574d8fe604a63c8b5df60f60e5982bf7ccbb1338ea527356" "checksum redox_syscall 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "c214e91d3ecf43e9a4e41e578973adeb14b474f2bee858742d127af75a0112b1" @@ -2489,6 +2610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rocket 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "a61d746c68f1d357f6e011985570474c4af368aa81900320074098d34ed0c64e" "checksum rocket_codegen 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7873d65adfa3e440ac373a28240341853da170913aad7e4207c0198389e5d0e9" "checksum rocket_contrib 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2348e3b2458e173203f1e51f3b2e00495a092b70bd9506d2ce2ac64e129d14" +"checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" "checksum rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395" "checksum rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0ceb8ce7a5e520de349e1fa172baeba4a9e8d5ef06c47471863530bc4972ee1e" "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" @@ -2528,6 +2650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum tempfile 3.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "55c1195ef8513f3273d55ff59fe5da6940287a0d7a98331254397f464833675b" "checksum thread-id 3.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +"checksum threadpool 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2f0c90a5f3459330ac8bc0d2f879c693bb7a2f59689c1083fc4ef83834da865" "checksum time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "d825be0eb33fda1a7e68012d51e9c7f451dc1a69391e7fdc197060bb8c56667b" "checksum tiny_http 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a442681f9f72e440be192700eeb2861e4174b9983f16f4877c93a134cb5e5f63" "checksum tokio 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "6e93c78d23cc61aa245a8acd2c4a79c4d7fa7fb5c3ca90d5737029f043a84895" @@ -2579,3 +2702,4 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum ws 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)" = "d2c221321dca56e6a80aa179d562e1fbe6ae116aeaa9205c76fa64e9e3c49dfc" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" "checksum yansi 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d60c3b48c9cdec42fb06b3b84b5b087405e1fa1c644a1af3930e4dfafe93de48" +"checksum yubico 0.4.0 (git+https://github.com/dani-garcia/yubico-rs)" = "" diff --git a/Cargo.toml b/Cargo.toml index 736a83b..60b60e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,9 @@ jsonwebtoken = "= 4.0.1" # U2F library u2f = "0.1.2" +# Yubico Library +yubico= { version = "0.4.0", default-features = false } + # A `dotenv` implementation for Rust dotenv = { version = "0.13.0", default-features = false } @@ -84,3 +87,6 @@ lettre_email = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' # Version 0.1.2 from crates.io lacks a commit that fixes a certificate error u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '193de35093a44' } + +# Allows optional libusb support +yubico = { git = 'https://github.com/dani-garcia/yubico-rs' } diff --git a/README.md b/README.md index ba4fed2..2de210a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward - [Enabling HTTPS](#enabling-https) - [Enabling WebSocket notifications](#enabling-websocket-notifications) - [Enabling U2F authentication](#enabling-u2f-authentication) + - [Enabling YubiKey OTP authentication](#enabling-yubikey-otp-authentication) - [Changing persistent data location](#changing-persistent-data-location) - [/data prefix:](#data-prefix) - [database name and location](#database-name-and-location) @@ -68,11 +69,11 @@ Basically full implementation of Bitwarden API is provided including: * Serving the static files for Vault interface * Website icons API * Authenticator and U2F support + * YubiKey OTP ## Missing features * Email confirmation * Other two-factor systems: - * YubiKey OTP (if your key supports U2F, you can use that) * Duo * Email codes @@ -252,6 +253,22 @@ docker run -d --name bitwarden \ Note that the value has to include the `https://` and it may include a port at the end (in the format of `https://bw.domain.tld:port`) when not using `443`. +### Enabling YubiKey OTP authentication +To enable YubiKey authentication, you must set the `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` env variables. + +If `YUBICO_SERVER` is not specified, it will use the default YubiCloud servers. You can generate `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` for the default YubiCloud [here](https://upgrade.yubico.com/getapikey/). + +Note: In order to generate API keys or use a YubiKey with an OTP server, it must be registered. After configuring your key in the [YubiKey Personalization Tool](https://www.yubico.com/products/services-software/personalization-tools/use/), you can register it with the default servers [here](https://upload.yubico.com/). + +```sh +docker run -d --name bitwarden \ + -e YUBICO_CLIENT_ID=12345 \ + -e YUBICO_SECRET_KEY=ABCDEABCDEABCDEABCDE= \ + -v /bw-data/:/data/ \ + -p 80:80 \ + mprasil/bitwarden:latest +``` + ### Changing persistent data location #### /data prefix: diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 3904acf..4c748fd 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -83,6 +83,9 @@ pub fn routes() -> Vec { generate_u2f_challenge, activate_u2f, activate_u2f_put, + generate_yubikey, + activate_yubikey, + activate_yubikey_put, get_organization, create_organization, diff --git a/src/api/core/two_factor.rs b/src/api/core/two_factor.rs index 969b8c5..d4be29b 100644 --- a/src/api/core/two_factor.rs +++ b/src/api/core/two_factor.rs @@ -491,3 +491,218 @@ pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> Api } err!("error verifying response") } + + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct EnableYubikeyData { + MasterPasswordHash: String, + Key1: Option, + Key2: Option, + Key3: Option, + Key4: Option, + Key5: Option, + Nfc: bool, +} + +#[derive(Deserialize, Serialize, Debug)] +#[allow(non_snake_case)] +struct YubikeyMetadata { + Keys: Vec, + Nfc: bool, +} + +use yubico::Yubico; +use yubico::config::Config; + +fn parse_yubikeys(data: &EnableYubikeyData) -> Vec { + let mut yubikeys: Vec = Vec::new(); + + if data.Key1.is_some() { + yubikeys.push(data.Key1.as_ref().unwrap().to_owned()); + } + + if data.Key2.is_some() { + yubikeys.push(data.Key2.as_ref().unwrap().to_owned()); + } + + if data.Key3.is_some() { + yubikeys.push(data.Key3.as_ref().unwrap().to_owned()); + } + + if data.Key4.is_some() { + yubikeys.push(data.Key4.as_ref().unwrap().to_owned()); + } + + if data.Key5.is_some() { + yubikeys.push(data.Key5.as_ref().unwrap().to_owned()); + } + + yubikeys +} + +fn jsonify_yubikeys(yubikeys: Vec) -> serde_json::Value { + let mut result = json!({}); + + for i in 0..yubikeys.len() { + let ref key = &yubikeys[i]; + result[format!("Key{}", i+1)] = Value::String(key.to_string()); + } + + result +} + +fn verify_yubikey_otp(otp: String) -> JsonResult { + if !CONFIG.yubico_cred_set { + err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. \ + Yubikey OTP Disabled") + } + + let yubico = Yubico::new(); + let config = Config::default().set_client_id(CONFIG.yubico_client_id.to_owned()).set_key(CONFIG.yubico_secret_key.to_owned()); + + let result = match CONFIG.yubico_server { + Some(ref server) => yubico.verify(otp, config.set_api_hosts(vec![server.to_owned()])), + None => yubico.verify(otp, config) + }; + + match result { + Ok(_answer) => Ok(Json(json!({}))), + Err(_e) => err!("Failed to verify OTP"), + } +} + +#[post("/two-factor/get-yubikey", data = "")] +fn generate_yubikey(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + if !CONFIG.yubico_cred_set { + err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. \ + Yubikey OTP Disabled") + } + + let data: PasswordData = data.into_inner().data; + + if !headers.user.check_valid_password(&data.MasterPasswordHash) { + err!("Invalid password"); + } + + let user_uuid = &headers.user.uuid; + let yubikey_type = TwoFactorType::YubiKey as i32; + + let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn); + + if let Some(r) = r { + let yubikey_metadata: YubikeyMetadata = + serde_json::from_str(&r.data).expect("Can't parse YubikeyMetadata data"); + + let mut result = jsonify_yubikeys(yubikey_metadata.Keys); + + result["Enabled"] = Value::Bool(true); + result["Nfc"] = Value::Bool(yubikey_metadata.Nfc); + result["Object"] = Value::String("twoFactorU2f".to_owned()); + + Ok(Json(result)) + } else { + Ok(Json(json!({ + "Enabled": false, + "Object": "twoFactorU2f", + }))) + } +} + +#[post("/two-factor/yubikey", data = "")] +fn activate_yubikey(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + let data: EnableYubikeyData = data.into_inner().data; + + if !headers.user.check_valid_password(&data.MasterPasswordHash) { + err!("Invalid password"); + } + + // Check if we already have some data + let yubikey_data = TwoFactor::find_by_user_and_type( + &headers.user.uuid, + TwoFactorType::YubiKey as i32, + &conn, + ); + + if let Some(yubikey_data) = yubikey_data { + yubikey_data.delete(&conn).expect("Error deleting current Yubikeys"); + } + + let yubikeys = parse_yubikeys(&data); + + if yubikeys.len() == 0 { + return Ok(Json(json!({ + "Enabled": false, + "Object": "twoFactorU2f", + }))); + } + + // Ensure they are valid OTPs + for yubikey in &yubikeys { + if yubikey.len() == 12 { + // YubiKey ID + continue + } + + let result = verify_yubikey_otp(yubikey.to_owned()); + + if let Err(_e) = result { + err!("Invalid Yubikey OTP provided"); + } + } + + let yubikey_ids: Vec = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect(); + + let yubikey_metadata = YubikeyMetadata { + Keys: yubikey_ids, + Nfc: data.Nfc, + }; + + let yubikey_registration = TwoFactor::new( + headers.user.uuid.clone(), + TwoFactorType::YubiKey, + serde_json::to_string(&yubikey_metadata).unwrap(), + ); + yubikey_registration + .save(&conn).expect("Failed to save Yubikey info"); + + let mut result = jsonify_yubikeys(yubikey_metadata.Keys); + + result["Enabled"] = Value::Bool(true); + result["Nfc"] = Value::Bool(yubikey_metadata.Nfc); + result["Object"] = Value::String("twoFactorU2f".to_owned()); + + Ok(Json(result)) +} + +#[put("/two-factor/yubikey", data = "")] +fn activate_yubikey_put(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + activate_yubikey(data, headers, conn) +} + +pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) -> ApiResult<()> { + if response.len() != 44 { + err!("Invalid Yubikey OTP length"); + } + + let yubikey_type = TwoFactorType::YubiKey as i32; + + let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn) { + Some(tf) => tf, + None => err!("No YubiKey devices registered"), + }; + + let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata"); + let response_id = &response[..12]; + + if !yubikey_metadata.Keys.contains(&response_id.to_owned()) { + err!("Given Yubikey is not registered"); + } + + let result = verify_yubikey_otp(response.to_owned()); + + match result { + Ok(_answer) => Ok(()), + Err(_e) => err!("Failed to verify Yubikey against OTP server"), + } +} diff --git a/src/api/identity.rs b/src/api/identity.rs index 175c5af..752bcd2 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -209,6 +209,12 @@ fn twofactor_auth( two_factor::validate_u2f_login(user_uuid, twofactor_code, conn)?; } + Some(TwoFactorType::YubiKey) => { + use api::core::two_factor; + + two_factor::validate_yubikey_login(user_uuid, twofactor_code, conn)?; + } + _ => err!("Invalid two factor provider"), } diff --git a/src/main.rs b/src/main.rs index db56066..feb241a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ extern crate oath; extern crate data_encoding; extern crate jsonwebtoken as jwt; extern crate u2f; +extern crate yubico; extern crate dotenv; #[macro_use] extern crate lazy_static; @@ -245,6 +246,11 @@ pub struct Config { domain: String, domain_set: bool, + yubico_cred_set: bool, + yubico_client_id: String, + yubico_secret_key: String, + yubico_server: Option, + mail: Option, } @@ -258,6 +264,9 @@ impl Config { let domain = get_env("DOMAIN"); + let yubico_client_id = get_env("YUBICO_CLIENT_ID"); + let yubico_secret_key = get_env("YUBICO_SECRET_KEY"); + Config { database_url: get_env_or("DATABASE_URL", format!("{}/{}", &df, "db.sqlite3")), icon_cache_folder: get_env_or("ICON_CACHE_FOLDER", format!("{}/{}", &df, "icon_cache")), @@ -283,6 +292,11 @@ impl Config { domain_set: domain.is_some(), domain: domain.unwrap_or("http://localhost".into()), + yubico_cred_set: yubico_client_id.is_some() && yubico_secret_key.is_some(), + yubico_client_id: yubico_client_id.unwrap_or("00000".into()), + yubico_secret_key: yubico_secret_key.unwrap_or("AAAAAAA".into()), + yubico_server: get_env("YUBICO_SERVER"), + mail: MailConfig::load(), } }