1 use rocket::Route;
2 use rocket_contrib::json::Json;
3 use serde_json::Value;
4 use yubico::{config::Config, verify};
5 
6 use crate::{
7     api::{core::two_factor::_generate_recover_code, EmptyResult, JsonResult, JsonUpcase, PasswordData},
8     auth::Headers,
9     db::{
10         models::{TwoFactor, TwoFactorType},
11         DbConn,
12     },
13     error::{Error, MapResult},
14     CONFIG,
15 };
16 
routes() -> Vec<Route>17 pub fn routes() -> Vec<Route> {
18     routes![generate_yubikey, activate_yubikey, activate_yubikey_put,]
19 }
20 
21 #[derive(Deserialize, Debug)]
22 #[allow(non_snake_case)]
23 struct EnableYubikeyData {
24     MasterPasswordHash: String,
25     Key1: Option<String>,
26     Key2: Option<String>,
27     Key3: Option<String>,
28     Key4: Option<String>,
29     Key5: Option<String>,
30     Nfc: bool,
31 }
32 
33 #[derive(Deserialize, Serialize, Debug)]
34 #[allow(non_snake_case)]
35 pub struct YubikeyMetadata {
36     Keys: Vec<String>,
37     pub Nfc: bool,
38 }
39 
parse_yubikeys(data: &EnableYubikeyData) -> Vec<String>40 fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
41     let data_keys = [&data.Key1, &data.Key2, &data.Key3, &data.Key4, &data.Key5];
42 
43     data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect()
44 }
45 
jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value46 fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
47     let mut result = json!({});
48 
49     for (i, key) in yubikeys.into_iter().enumerate() {
50         result[format!("Key{}", i + 1)] = Value::String(key);
51     }
52 
53     result
54 }
55 
get_yubico_credentials() -> Result<(String, String), Error>56 fn get_yubico_credentials() -> Result<(String, String), Error> {
57     if !CONFIG._enable_yubico() {
58         err!("Yubico support is disabled");
59     }
60 
61     match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
62         (Some(id), Some(secret)) => Ok((id, secret)),
63         _ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"),
64     }
65 }
66 
verify_yubikey_otp(otp: String) -> EmptyResult67 fn verify_yubikey_otp(otp: String) -> EmptyResult {
68     let (yubico_id, yubico_secret) = get_yubico_credentials()?;
69 
70     let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
71 
72     match CONFIG.yubico_server() {
73         Some(server) => verify(otp, config.set_api_hosts(vec![server])),
74         None => verify(otp, config),
75     }
76     .map_res("Failed to verify OTP")
77     .and(Ok(()))
78 }
79 
80 #[post("/two-factor/get-yubikey", data = "<data>")]
generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult81 fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
82     // Make sure the credentials are set
83     get_yubico_credentials()?;
84 
85     let data: PasswordData = data.into_inner().data;
86     let user = headers.user;
87 
88     if !user.check_valid_password(&data.MasterPasswordHash) {
89         err!("Invalid password");
90     }
91 
92     let user_uuid = &user.uuid;
93     let yubikey_type = TwoFactorType::YubiKey as i32;
94 
95     let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn);
96 
97     if let Some(r) = r {
98         let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
99 
100         let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
101 
102         result["Enabled"] = Value::Bool(true);
103         result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
104         result["Object"] = Value::String("twoFactorU2f".to_owned());
105 
106         Ok(Json(result))
107     } else {
108         Ok(Json(json!({
109             "Enabled": false,
110             "Object": "twoFactorU2f",
111         })))
112     }
113 }
114 
115 #[post("/two-factor/yubikey", data = "<data>")]
activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult116 fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
117     let data: EnableYubikeyData = data.into_inner().data;
118     let mut user = headers.user;
119 
120     if !user.check_valid_password(&data.MasterPasswordHash) {
121         err!("Invalid password");
122     }
123 
124     // Check if we already have some data
125     let mut yubikey_data = match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn) {
126         Some(data) => data,
127         None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
128     };
129 
130     let yubikeys = parse_yubikeys(&data);
131 
132     if yubikeys.is_empty() {
133         return Ok(Json(json!({
134             "Enabled": false,
135             "Object": "twoFactorU2f",
136         })));
137     }
138 
139     // Ensure they are valid OTPs
140     for yubikey in &yubikeys {
141         if yubikey.len() == 12 {
142             // YubiKey ID
143             continue;
144         }
145 
146         verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
147     }
148 
149     let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
150 
151     let yubikey_metadata = YubikeyMetadata {
152         Keys: yubikey_ids,
153         Nfc: data.Nfc,
154     };
155 
156     yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
157     yubikey_data.save(&conn)?;
158 
159     _generate_recover_code(&mut user, &conn);
160 
161     let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
162 
163     result["Enabled"] = Value::Bool(true);
164     result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
165     result["Object"] = Value::String("twoFactorU2f".to_owned());
166 
167     Ok(Json(result))
168 }
169 
170 #[put("/two-factor/yubikey", data = "<data>")]
activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult171 fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
172     activate_yubikey(data, headers, conn)
173 }
174 
validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult175 pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
176     if response.len() != 44 {
177         err!("Invalid Yubikey OTP length");
178     }
179 
180     let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata");
181     let response_id = &response[..12];
182 
183     if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
184         err!("Given Yubikey is not registered");
185     }
186 
187     let result = verify_yubikey_otp(response.to_owned());
188 
189     match result {
190         Ok(_answer) => Ok(()),
191         Err(_e) => err!("Failed to verify Yubikey against OTP server"),
192     }
193 }
194