Implemented U2F, refactored Two Factor authentication, registering U2F device and authentication should work. Works on Chrome on MacOS with a virtual device.

This commit is contained in:
Daniel García
2018-07-12 21:46:50 +02:00
parent dde7c0d99b
commit dae92b9018
17 changed files with 816 additions and 272 deletions

263
Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -16,8 +16,8 @@ reqwest = "0.8.6"
multipart = "0.14.2"
# A generic serialization/deserialization framework
serde = "1.0.68"
serde_derive = "1.0.68"
serde = "1.0.70"
serde_derive = "1.0.70"
serde_json = "1.0.22"
# A safe, extensible ORM and Query builder
@@ -45,11 +45,22 @@ data-encoding = "2.1.1"
# JWT library
jsonwebtoken = "= 4.0.1"
# U2F library
u2f = "0.1.2"
# A `dotenv` implementation for Rust
dotenv = { version = "0.13.0", default-features = false }
# Lazy static macro
lazy_static = "1.0.1"
# Numerical libraries
num-traits = "0.2.5"
num-derive = "0.2.2"
[patch.crates-io]
jsonwebtoken = { path = "libs/jsonwebtoken" } # Make jwt use ring 0.11, to match rocket
# Make jwt use ring 0.11, to match rocket
jsonwebtoken = { path = "libs/jsonwebtoken" }
# 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' }

View File

@@ -0,0 +1,8 @@
UPDATE users
SET totp_secret = (
SELECT twofactor.data FROM twofactor
WHERE twofactor.type = 0
AND twofactor.user_uuid = users.uuid
);
DROP TABLE twofactor;

View File

@@ -0,0 +1,15 @@
CREATE TABLE twofactor (
uuid TEXT NOT NULL PRIMARY KEY,
user_uuid TEXT NOT NULL REFERENCES users (uuid),
type INTEGER NOT NULL,
enabled BOOLEAN NOT NULL,
data TEXT NOT NULL,
UNIQUE (user_uuid, type)
);
INSERT INTO twofactor (uuid, user_uuid, type, enabled, data)
SELECT lower(hex(randomblob(16))) , uuid, 0, 1, u.totp_secret FROM users u where u.totp_secret IS NOT NULL;
UPDATE users SET totp_secret = NULL; -- Instead of recreating the table, just leave the columns empty

View File

@@ -2,7 +2,7 @@ mod accounts;
mod ciphers;
mod folders;
mod organizations;
mod two_factor;
pub(crate) mod two_factor;
use self::accounts::*;
use self::ciphers::*;
@@ -58,9 +58,11 @@ pub fn routes() -> Vec<Route> {
get_twofactor,
get_recover,
recover,
disable_twofactor,
generate_authenticator,
activate_authenticator,
disable_authenticator,
generate_u2f,
activate_u2f,
get_organization,
create_organization,

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,21 @@
use std::collections::HashMap;
use rocket::{Route, Outcome};
use rocket::request::{self, Request, FromRequest, Form, FormItems, FromForm};
use rocket::request::{self, Form, FormItems, FromForm, FromRequest, Request};
use rocket::{Outcome, Route};
use rocket_contrib::{Json, Value};
use db::DbConn;
use num_traits::FromPrimitive;
use db::models::*;
use db::DbConn;
use util;
use util::{self, JsonMap};
use api::JsonResult;
use api::{ApiResult, JsonResult};
pub fn routes() -> Vec<Route> {
routes![ login]
routes![login]
}
#[post("/connect/token", data = "<connect_data>")]
@@ -21,8 +23,8 @@ fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn)
let data = connect_data.get();
match data.grant_type {
GrantType::RefreshToken =>_refresh_login(data, device_type, conn),
GrantType::Password => _password_login(data, device_type, conn)
GrantType::RefreshToken => _refresh_login(data, device_type, conn),
GrantType::Password => _password_login(data, device_type, conn),
}
}
@@ -33,7 +35,7 @@ fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) ->
// Get device by refresh token
let mut device = match Device::find_by_refresh_token(token, &conn) {
Some(device) => device,
None => err!("Invalid refresh token")
None => err!("Invalid refresh token"),
};
// COMMON
@@ -64,7 +66,7 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) ->
let username = data.get("username");
let user = match User::find_by_mail(username, &conn) {
Some(user) => user,
None => err!("Username or password is incorrect. Try again.")
None => err!("Username or password is incorrect. Try again."),
};
// Check password
@@ -72,7 +74,7 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) ->
if !user.check_valid_password(password) {
err!("Username or password is incorrect. Try again.")
}
// Let's only use the header and ignore the 'devicetype' parameter
let device_type_num = device_type.0;
@@ -102,42 +104,7 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) ->
}
};
let twofactor_token = if user.requires_twofactor() {
let twofactor_provider = util::parse_option_string(data.get_opt("twoFactorProvider")).unwrap_or(0);
let twofactor_code = match data.get_opt("twoFactorToken") {
Some(code) => code,
None => err_json!(_json_err_twofactor())
};
match twofactor_provider {
0 /* TOTP */ => {
let totp_code: u64 = match twofactor_code.parse() {
Ok(code) => code,
Err(_) => err!("Invalid Totp code")
};
if !user.check_totp_code(totp_code) {
err_json!(_json_err_twofactor())
}
if util::parse_option_string(data.get_opt("twoFactorRemember")).unwrap_or(0) == 1 {
device.refresh_twofactor_remember();
device.twofactor_remember.clone()
} else {
device.delete_twofactor_remember();
None
}
},
5 /* Remember */ => {
match device.twofactor_remember {
Some(ref remember) if remember == twofactor_code => (),
_ => err_json!(_json_err_twofactor())
};
None // No twofactor token needed here
},
_ => err!("Invalid two factor provider"),
}
} else { None }; // No twofactor token if twofactor is disabled
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?;
// Common
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
@@ -163,13 +130,124 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) ->
Ok(Json(result))
}
fn _json_err_twofactor() -> Value {
json!({
fn twofactor_auth(
user_uuid: &str,
data: &ConnectData,
device: &mut Device,
conn: &DbConn,
) -> ApiResult<Option<String>> {
let twofactors_raw = TwoFactor::find_by_user(user_uuid, conn);
// Remove u2f challenge twofactors (impl detail)
let twofactors: Vec<_> = twofactors_raw.iter().filter(|tf| tf.type_ < 1000).collect();
let providers: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
// No twofactor token if twofactor is disabled
if twofactors.len() == 0 {
return Ok(None);
}
let provider = match util::parse_option_string(data.get_opt("twoFactorProvider")) {
Some(provider) => provider,
None => providers[0], // If we aren't given a two factor provider, asume the first one
};
let twofactor_code = match data.get_opt("twoFactorToken") {
Some(code) => code,
None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
};
let twofactor = twofactors.iter().filter(|tf| tf.type_ == provider).nth(0);
match TwoFactorType::from_i32(provider) {
Some(TwoFactorType::Remember) => {
match &device.twofactor_remember {
Some(remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here
_ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
}
}
Some(TwoFactorType::Authenticator) => {
let twofactor = match twofactor {
Some(tf) => tf,
None => err!("TOTP not enabled"),
};
let totp_code: u64 = match twofactor_code.parse() {
Ok(code) => code,
_ => err!("Invalid TOTP code"),
};
if !twofactor.check_totp_code(totp_code) {
err_json!(_json_err_twofactor(&providers, user_uuid, conn)?)
}
}
Some(TwoFactorType::U2f) => {
use api::core::two_factor;
two_factor::validate_u2f_login(user_uuid, twofactor_code, conn)?;
}
_ => err!("Invalid two factor provider"),
}
if util::parse_option_string(data.get_opt("twoFactorRemember")).unwrap_or(0) == 1 {
Ok(Some(device.refresh_twofactor_remember()))
} else {
device.delete_twofactor_remember();
Ok(None)
}
}
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
use api::core::two_factor;
let mut result = json!({
"error" : "invalid_grant",
"error_description" : "Two factor required.",
"TwoFactorProviders" : [ 0 ],
"TwoFactorProviders2" : { "0" : null }
})
"TwoFactorProviders" : providers,
"TwoFactorProviders2" : {} // { "0" : null }
});
for provider in providers {
result["TwoFactorProviders2"][provider.to_string()] = Value::Null;
match TwoFactorType::from_i32(*provider) {
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
Some(TwoFactorType::U2f) => {
let request = two_factor::generate_u2f_login(user_uuid, conn)?;
let mut challenge_list = Vec::new();
for key in request.registered_keys {
let mut challenge_map = JsonMap::new();
challenge_map.insert("appId".into(), Value::String(request.app_id.clone()));
challenge_map
.insert("challenge".into(), Value::String(request.challenge.clone()));
challenge_map.insert("version".into(), Value::String(key.version));
challenge_map.insert(
"keyHandle".into(),
Value::String(key.key_handle.unwrap_or_default()),
);
challenge_list.push(Value::Object(challenge_map));
}
let mut map = JsonMap::new();
use serde_json;
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
map.insert("Challenges".into(), Value::String(challenge_list_str));
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
}
_ => {}
}
}
Ok(result)
}
#[derive(Clone, Copy)]
@@ -187,7 +265,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for DeviceType {
}
}
#[derive(Debug)]
struct ConnectData {
grant_type: GrantType,
@@ -196,7 +273,10 @@ struct ConnectData {
}
#[derive(Debug, Copy, Clone)]
enum GrantType { RefreshToken, Password }
enum GrantType {
RefreshToken,
Password,
}
impl ConnectData {
fn get(&self, key: &str) -> &String {
@@ -227,25 +307,28 @@ impl<'f> FromForm<'f> for ConnectData {
}
// Validate needed values
let (grant_type, is_device) =
match data.get("grant_type").map(String::as_ref) {
Some("refresh_token") => {
check_values(&data, &VALUES_REFRESH)?;
(GrantType::RefreshToken, false) // Device doesn't matter here
}
Some("password") => {
check_values(&data, &VALUES_PASSWORD)?;
let (grant_type, is_device) = match data.get("grant_type").map(String::as_ref) {
Some("refresh_token") => {
check_values(&data, &VALUES_REFRESH)?;
(GrantType::RefreshToken, false) // Device doesn't matter here
}
Some("password") => {
check_values(&data, &VALUES_PASSWORD)?;
let is_device = match data["client_id"].as_ref() {
"browser" | "mobile" => check_values(&data, &VALUES_DEVICE)?,
_ => false
};
(GrantType::Password, is_device)
}
_ => return Err("Grant type not supported".to_string())
};
let is_device = match data["client_id"].as_ref() {
"browser" | "mobile" => check_values(&data, &VALUES_DEVICE)?,
_ => false,
};
(GrantType::Password, is_device)
}
_ => return Err("Grant type not supported".to_string()),
};
Ok(ConnectData { grant_type, is_device, data })
Ok(ConnectData {
grant_type,
is_device,
data,
})
}
}

View File

@@ -1,4 +1,4 @@
mod core;
pub(crate) mod core;
mod icons;
mod identity;
mod web;
@@ -12,8 +12,9 @@ use rocket::response::status::BadRequest;
use rocket_contrib::Json;
// Type aliases for API methods results
type JsonResult = Result<Json, BadRequest<Json>>;
type EmptyResult = Result<(), BadRequest<Json>>;
type ApiResult<T> = Result<T, BadRequest<Json>>;
type JsonResult = ApiResult<Json>;
type EmptyResult = ApiResult<()>;
use util;
type JsonUpcase<T> = Json<util::UpCase<T>>;

View File

@@ -4,13 +4,13 @@ use std::path::{Path, PathBuf};
use rocket::request::Request;
use rocket::response::{self, NamedFile, Responder};
use rocket::Route;
use rocket_contrib::Json;
use rocket_contrib::{Json, Value};
use CONFIG;
pub fn routes() -> Vec<Route> {
if CONFIG.web_vault_enabled {
routes![web_index, web_files, attachments, alive]
routes![web_index, app_id, web_files, attachments, alive]
} else {
routes![attachments, alive]
}
@@ -22,6 +22,20 @@ fn web_index() -> WebHeaders<io::Result<NamedFile>> {
web_files("index.html".into())
}
#[get("/app-id.json")]
fn app_id() -> WebHeaders<Json<Value>> {
WebHeaders(Json(json!({
"trustedFacets": [
{
"version": { "major": 1, "minor": 0 },
"ids": [
&CONFIG.domain,
"ios:bundle-id:com.8bit.bitwarden",
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ]
}]
})))
}
#[get("/<p..>", rank = 1)] // Only match this if the other routes don't match
fn web_files(p: PathBuf) -> WebHeaders<io::Result<NamedFile>> {
WebHeaders(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join(p)))

View File

@@ -11,11 +11,11 @@ use serde::ser::Serialize;
use CONFIG;
const JWT_ALGORITHM: jwt::Algorithm = jwt::Algorithm::RS256;
// TODO: This isn't used, but we should make sure it represents the correct address
pub const JWT_ISSUER: &str = "localhost:8000/identity";
lazy_static! {
pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2);
pub static ref JWT_ISSUER: String = CONFIG.domain.clone();
static ref JWT_HEADER: jwt::Header = jwt::Header::new(JWT_ALGORITHM);
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key) {
@@ -43,7 +43,7 @@ pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
validate_iat: true,
validate_nbf: true,
aud: None,
iss: Some(JWT_ISSUER.into()),
iss: Some(JWT_ISSUER.clone()),
sub: None,
algorithms: vec![JWT_ALGORITHM],
};

View File

@@ -43,11 +43,14 @@ impl Device {
}
}
pub fn refresh_twofactor_remember(&mut self) {
pub fn refresh_twofactor_remember(&mut self) -> String {
use data_encoding::BASE64;
use crypto;
self.twofactor_remember = Some(BASE64.encode(&crypto::get_random(vec![0u8; 180])));
let twofactor_remember = BASE64.encode(&crypto::get_random(vec![0u8; 180]));
self.twofactor_remember = Some(twofactor_remember.clone());
twofactor_remember
}
pub fn delete_twofactor_remember(&mut self) {

View File

@@ -6,6 +6,7 @@ mod user;
mod collection;
mod organization;
mod two_factor;
pub use self::attachment::Attachment;
pub use self::cipher::Cipher;
@@ -15,3 +16,4 @@ pub use self::user::User;
pub use self::organization::Organization;
pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType};
pub use self::collection::{Collection, CollectionUser, CollectionCipher};
pub use self::two_factor::{TwoFactor, TwoFactorType};

112
src/db/models/two_factor.rs Normal file
View File

@@ -0,0 +1,112 @@
use serde_json::Value as JsonValue;
use uuid::Uuid;
use super::User;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "twofactor"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[primary_key(uuid)]
pub struct TwoFactor {
pub uuid: String,
pub user_uuid: String,
pub type_: i32,
pub enabled: bool,
pub data: String,
}
#[allow(dead_code)]
#[derive(FromPrimitive, ToPrimitive)]
pub enum TwoFactorType {
Authenticator = 0,
Email = 1,
Duo = 2,
YubiKey = 3,
U2f = 4,
Remember = 5,
OrganizationDuo = 6,
// These are implementation details
U2fRegisterChallenge = 1000,
U2fLoginChallenge = 1001,
}
/// Local methods
impl TwoFactor {
pub fn new(user_uuid: String, type_: TwoFactorType, data: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
user_uuid,
type_: type_ as i32,
enabled: true,
data,
}
}
pub fn check_totp_code(&self, totp_code: u64) -> bool {
let totp_secret = self.data.as_bytes();
use data_encoding::BASE32;
use oath::{totp_raw_now, HashType};
let decoded_secret = match BASE32.decode(totp_secret) {
Ok(s) => s,
Err(_) => return false
};
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
generated == totp_code
}
pub fn to_json(&self) -> JsonValue {
json!({
"Enabled": self.enabled,
"Key": "", // This key and value vary
"Object": "twoFactorAuthenticator" // This value varies
})
}
pub fn to_json_list(&self) -> JsonValue {
json!({
"Enabled": self.enabled,
"Type": self.type_,
"Object": "twoFactorProvider"
})
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::twofactor;
/// Database methods
impl TwoFactor {
pub fn save(&self, conn: &DbConn) -> QueryResult<usize> {
diesel::replace_into(twofactor::table)
.values(self)
.execute(&**conn)
}
pub fn delete(self, conn: &DbConn) -> QueryResult<usize> {
diesel::delete(
twofactor::table.filter(
twofactor::uuid.eq(self.uuid)
)
).execute(&**conn)
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
.load::<Self>(&**conn).expect("Error loading twofactor")
}
pub fn find_by_user_and_type(user_uuid: &str, type_: i32, conn: &DbConn) -> Option<Self> {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
.filter(twofactor::type_.eq(type_))
.first::<Self>(&**conn).ok()
}
}

View File

@@ -27,7 +27,8 @@ pub struct User {
pub private_key: Option<String>,
pub public_key: Option<String>,
pub totp_secret: Option<String>,
#[column_name = "totp_secret"]
_totp_secret: Option<String>,
pub totp_recover: Option<String>,
pub security_stamp: String,
@@ -64,7 +65,7 @@ impl User {
private_key: None,
public_key: None,
totp_secret: None,
_totp_secret: None,
totp_recover: None,
equivalent_domains: "[]".to_string(),
@@ -97,28 +98,6 @@ impl User {
pub fn reset_security_stamp(&mut self) {
self.security_stamp = Uuid::new_v4().to_string();
}
pub fn requires_twofactor(&self) -> bool {
self.totp_secret.is_some()
}
pub fn check_totp_code(&self, totp_code: u64) -> bool {
if let Some(ref totp_secret) = self.totp_secret {
// Validate totp
use data_encoding::BASE32;
use oath::{totp_raw_now, HashType};
let decoded_secret = match BASE32.decode(totp_secret.as_bytes()) {
Ok(s) => s,
Err(_) => return false
};
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
generated == totp_code
} else {
true
}
}
}
use diesel;
@@ -130,10 +109,13 @@ use db::schema::users;
impl User {
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
use super::UserOrganization;
use super::TwoFactor;
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
let orgs_json: Vec<JsonValue> = orgs.iter().map(|c| c.to_json(&conn)).collect();
let twofactor_enabled = TwoFactor::find_by_user(&self.uuid, conn).len() > 0;
json!({
"Id": self.uuid,
"Name": self.name,
@@ -142,7 +124,7 @@ impl User {
"Premium": true,
"MasterPasswordHint": self.password_hint,
"Culture": "en-US",
"TwoFactorEnabled": self.totp_secret.is_some(),
"TwoFactorEnabled": twofactor_enabled,
"Key": self.key,
"PrivateKey": self.private_key,
"SecurityStamp": self.security_stamp,

View File

@@ -79,6 +79,17 @@ table! {
}
}
table! {
twofactor (uuid) {
uuid -> Text,
user_uuid -> Text,
#[sql_name = "type"]
type_ -> Integer,
enabled -> Bool,
data -> Text,
}
}
table! {
users (uuid) {
uuid -> Text,
@@ -132,6 +143,7 @@ joinable!(devices -> users (user_uuid));
joinable!(folders -> users (user_uuid));
joinable!(folders_ciphers -> ciphers (cipher_uuid));
joinable!(folders_ciphers -> folders (folder_uuid));
joinable!(twofactor -> users (user_uuid));
joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid));
@@ -146,6 +158,7 @@ allow_tables_to_appear_in_same_query!(
folders,
folders_ciphers,
organizations,
twofactor,
users,
users_collections,
users_organizations,

View File

@@ -19,9 +19,13 @@ extern crate chrono;
extern crate oath;
extern crate data_encoding;
extern crate jsonwebtoken as jwt;
extern crate u2f;
extern crate dotenv;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate num_derive;
extern crate num_traits;
use std::{env, path::Path, process::{exit, Command}};
use rocket::Rocket;
@@ -160,6 +164,7 @@ pub struct Config {
local_icon_extractor: bool,
signups_allowed: bool,
password_iterations: i32,
domain: String,
}
impl Config {
@@ -184,6 +189,7 @@ impl Config {
local_icon_extractor: util::parse_option_string(env::var("LOCAL_ICON_EXTRACTOR").ok()).unwrap_or(false),
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(true),
password_iterations: util::parse_option_string(env::var("PASSWORD_ITERATIONS").ok()).unwrap_or(100_000),
domain: env::var("DOMAIN").unwrap_or("https://localhost".into()),
}
}
}

View File

@@ -132,7 +132,9 @@ pub fn format_date(date: &NaiveDateTime) -> String {
use std::fmt;
use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, SeqAccess, Visitor};
use serde_json::Value;
use serde_json::{self, Value};
pub type JsonMap = serde_json::Map<String, Value>;
#[derive(PartialEq, Serialize, Deserialize)]
pub struct UpCase<T: DeserializeOwned> {
@@ -162,8 +164,7 @@ impl<'de> Visitor<'de> for UpCaseVisitor {
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where A: MapAccess<'de>
{
use serde_json::Map;
let mut result_map = Map::<String, Value>::new();
let mut result_map = JsonMap::new();
while let Some((key, value)) = map.next_entry()? {
result_map.insert(upcase_first(key), upcase_value(&value));