1
0
Fork 0
mirror of https://github.com/dani-garcia/vaultwarden.git synced 2025-08-27 21:14:47 +00:00

First working version

This commit is contained in:
Daniel García 2018-02-10 01:00:55 +01:00
commit 5cd40c63ed
172 changed files with 17903 additions and 0 deletions

60
src/db/mod.rs Normal file
View file

@ -0,0 +1,60 @@
use std::ops::Deref;
use diesel::{Connection as DieselConnection, ConnectionError};
use diesel::sqlite::SqliteConnection;
use r2d2;
use r2d2_diesel::ConnectionManager;
use rocket::http::Status;
use rocket::request::{self, FromRequest};
use rocket::{Outcome, Request, State};
use CONFIG;
/// An alias to the database connection used
type Connection = SqliteConnection;
/// An alias to the type for a pool of Diesel SQLite connections.
type Pool = r2d2::Pool<ConnectionManager<Connection>>;
/// Connection request guard type: a wrapper around an r2d2 pooled connection.
pub struct DbConn(pub r2d2::PooledConnection<ConnectionManager<Connection>>);
pub mod schema;
pub mod models;
/// Initializes a database pool.
pub fn init_pool() -> Pool {
let manager = ConnectionManager::new(&*CONFIG.database_url);
r2d2::Pool::builder()
.build(manager)
.expect("Failed to create pool")
}
pub fn get_connection() -> Result<Connection, ConnectionError> {
Connection::establish(&CONFIG.database_url)
}
/// Attempts to retrieve a single connection from the managed database pool. If
/// no pool is currently managed, fails with an `InternalServerError` status. If
/// no connections are available, fails with a `ServiceUnavailable` status.
impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<DbConn, ()> {
let pool = request.guard::<State<Pool>>()?;
match pool.get() {
Ok(conn) => Outcome::Success(DbConn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ()))
}
}
}
// For the convenience of using an &DbConn as a &Database.
impl Deref for DbConn {
type Target = Connection;
fn deref(&self) -> &Self::Target {
&self.0
}
}

112
src/db/models/cipher.rs Normal file
View file

@ -0,0 +1,112 @@
use chrono::{NaiveDate, NaiveDateTime, Utc};
use time::Duration;
use serde_json::Value as JsonValue;
use uuid::Uuid;
#[derive(Queryable, Insertable, Identifiable)]
#[table_name = "ciphers"]
#[primary_key(uuid)]
pub struct Cipher {
pub uuid: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: String,
pub folder_uuid: Option<String>,
pub organization_uuid: Option<String>,
// Login = 1,
// SecureNote = 2,
// Card = 3,
// Identity = 4
pub type_: i32,
pub data: String,
pub favorite: bool,
pub attachments: Option<Vec<u8>>,
}
/// Local methods
impl Cipher {
pub fn new(user_uuid: String, type_: i32, favorite: bool) -> Cipher {
let now = Utc::now().naive_utc();
Cipher {
uuid: Uuid::new_v4().to_string(),
created_at: now,
updated_at: now,
user_uuid,
folder_uuid: None,
organization_uuid: None,
type_,
favorite,
data: String::new(),
attachments: None,
}
}
pub fn to_json(&self) -> JsonValue {
use serde_json;
use util::format_date;
let data: JsonValue = serde_json::from_str(&self.data).unwrap();
json!({
"Id": self.uuid,
"Type": self.type_,
"RevisionDate": format_date(&self.updated_at),
"FolderId": self.folder_uuid,
"Favorite": self.favorite,
"OrganizationId": "",
"Attachments": self.attachments,
"OrganizationUseTotp": false,
"Data": data,
"Object": "cipher",
"Edit": true,
})
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::ciphers;
/// Database methods
impl Cipher {
pub fn save(&self, conn: &DbConn) -> bool {
// TODO: Update modified date
match diesel::replace_into(ciphers::table)
.values(self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
}
pub fn delete(self, conn: &DbConn) -> bool {
match diesel::delete(ciphers::table.filter(
ciphers::uuid.eq(self.uuid)))
.execute(&**conn) {
Ok(1) => true, // One row deleted
_ => false,
}
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Cipher> {
ciphers::table
.filter(ciphers::uuid.eq(uuid))
.first::<Cipher>(&**conn).ok()
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Cipher> {
ciphers::table
.filter(ciphers::user_uuid.eq(user_uuid))
.load::<Cipher>(&**conn).expect("Error loading ciphers")
}
}

117
src/db/models/device.rs Normal file
View file

@ -0,0 +1,117 @@
use chrono::{NaiveDate, NaiveDateTime, Utc};
use time::Duration;
use serde_json::Value as JsonValue;
use uuid::Uuid;
#[derive(Queryable, Insertable, Identifiable)]
#[table_name = "devices"]
#[primary_key(uuid)]
pub struct Device {
pub uuid: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: String,
pub name: String,
/// https://github.com/bitwarden/core/tree/master/src/Core/Enums
pub type_: i32,
pub push_token: Option<String>,
pub refresh_token: String,
}
/// Local methods
impl Device {
pub fn new(uuid: String, user_uuid: String, name: String, type_: i32) -> Device {
let now = Utc::now().naive_utc();
Device {
uuid,
created_at: now,
updated_at: now,
user_uuid,
name,
type_,
push_token: None,
refresh_token: String::new(),
}
}
pub fn refresh_tokens(&mut self, user: &super::User) -> (String, i64) {
// If there is no refresh token, we create one
if self.refresh_token.is_empty() {
use data_encoding::BASE64URL;
use crypto;
self.refresh_token = BASE64URL.encode(&crypto::get_random_64());
}
// Update the expiration of the device and the last update date
let time_now = Utc::now().naive_utc();
self.updated_at = time_now;
// Create the JWT claims struct, to send to the client
use auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER};
let claims = JWTClaims {
nbf: time_now.timestamp(),
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
iss: JWT_ISSUER.to_string(),
sub: user.uuid.to_string(),
premium: true,
name: user.name.to_string(),
email: user.email.to_string(),
email_verified: true,
sstamp: user.security_stamp.to_string(),
device: self.uuid.to_string(),
scope: vec!["api".into(), "offline_access".into()],
amr: vec!["Application".into()],
};
(encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds())
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::devices;
/// Database methods
impl Device {
pub fn save(&self, conn: &DbConn) -> bool {
// TODO: Update modified date
match diesel::replace_into(devices::table)
.values(self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
}
pub fn delete(self, conn: &DbConn) -> bool {
match diesel::delete(devices::table.filter(
devices::uuid.eq(self.uuid)))
.execute(&**conn) {
Ok(1) => true, // One row deleted
_ => false,
}
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Device> {
devices::table
.filter(devices::uuid.eq(uuid))
.first::<Device>(&**conn).ok()
}
pub fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option<Device> {
devices::table
.filter(devices::refresh_token.eq(refresh_token))
.first::<Device>(&**conn).ok()
}
}

83
src/db/models/folder.rs Normal file
View file

@ -0,0 +1,83 @@
use chrono::{NaiveDate, NaiveDateTime, Utc};
use time::Duration;
use serde_json::Value as JsonValue;
use uuid::Uuid;
#[derive(Queryable, Insertable, Identifiable)]
#[table_name = "folders"]
#[primary_key(uuid)]
pub struct Folder {
pub uuid: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: String,
pub name: String,
}
/// Local methods
impl Folder {
pub fn new(user_uuid: String, name: String) -> Folder {
let now = Utc::now().naive_utc();
Folder {
uuid: Uuid::new_v4().to_string(),
created_at: now,
updated_at: now,
user_uuid,
name,
}
}
pub fn to_json(&self) -> JsonValue {
use util::format_date;
json!({
"Id": self.uuid,
"RevisionDate": format_date(&self.updated_at),
"Name": self.name,
"Object": "folder",
})
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::folders;
/// Database methods
impl Folder {
pub fn save(&self, conn: &DbConn) -> bool {
// TODO: Update modified date
match diesel::replace_into(folders::table)
.values(self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
}
pub fn delete(self, conn: &DbConn) -> bool {
match diesel::delete(folders::table.filter(
folders::uuid.eq(self.uuid)))
.execute(&**conn) {
Ok(1) => true, // One row deleted
_ => false,
}
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Folder> {
folders::table
.filter(folders::uuid.eq(uuid))
.first::<Folder>(&**conn).ok()
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Folder> {
folders::table
.filter(folders::user_uuid.eq(user_uuid))
.load::<Folder>(&**conn).expect("Error loading folders")
}
}

9
src/db/models/mod.rs Normal file
View file

@ -0,0 +1,9 @@
mod cipher;
mod device;
mod folder;
mod user;
pub use self::cipher::Cipher;
pub use self::device::Device;
pub use self::folder::Folder;
pub use self::user::User;

159
src/db/models/user.rs Normal file
View file

@ -0,0 +1,159 @@
use chrono::{NaiveDate, NaiveDateTime, Utc};
use time::Duration;
use serde_json::Value as JsonValue;
use uuid::Uuid;
use CONFIG;
#[derive(Queryable, Insertable, Identifiable)]
#[table_name = "users"]
#[primary_key(uuid)]
pub struct User {
pub uuid: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub email: String,
pub name: String,
pub password_hash: Vec<u8>,
pub salt: Vec<u8>,
pub password_iterations: i32,
pub password_hint: Option<String>,
pub key: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub totp_secret: Option<String>,
pub totp_recover: Option<String>,
pub security_stamp: String,
}
/// Local methods
impl User {
pub fn new(mail: String, key: String, password: String) -> User {
let now = Utc::now().naive_utc();
let email = mail.to_lowercase();
use crypto;
let iterations = CONFIG.password_iterations;
let salt = crypto::get_random_64();
let password_hash = crypto::hash_password(password.as_bytes(), &salt, iterations as u32);
User {
uuid: Uuid::new_v4().to_string(),
created_at: now,
updated_at: now,
name: email.clone(),
email,
key,
password_hash,
salt,
password_iterations: iterations,
security_stamp: Uuid::new_v4().to_string(),
password_hint: None,
private_key: None,
public_key: None,
totp_secret: None,
totp_recover: None,
}
}
pub fn check_valid_password(&self, password: &str) -> bool {
use crypto;
crypto::verify_password_hash(password.as_bytes(),
&self.salt,
&self.password_hash,
self.password_iterations as u32)
}
pub fn set_password(&mut self, password: &str) {
use crypto;
self.password_hash = crypto::hash_password(password.as_bytes(),
&self.salt,
self.password_iterations as u32);
self.reset_security_stamp();
}
pub fn reset_security_stamp(&mut self) {
self.security_stamp = Uuid::new_v4().to_string();
}
pub fn check_totp_code(&self, totp_code: Option<u64>) -> bool {
if let Some(ref totp_secret) = self.totp_secret {
if let Some(code) = totp_code {
// 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(e) => return false
};
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
generated == code
} else {
false
}
} else {
true
}
}
pub fn to_json(&self) -> JsonValue {
json!({
"Id": self.uuid,
"Name": self.name,
"Email": self.email,
"EmailVerified": true,
"Premium": true,
"MasterPasswordHint": self.password_hint,
"Culture": "en-US",
"TwoFactorEnabled": self.totp_secret.is_some(),
"Key": self.key,
"PrivateKey": self.private_key,
"SecurityStamp": self.security_stamp,
"Organizations": [],
"Object": "profile"
})
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::users;
/// Database methods
impl User {
pub fn save(&self, conn: &DbConn) -> bool {
// TODO: Update modified date
match diesel::replace_into(users::table) // Insert or update
.values(self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
}
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<User> {
let lower_mail = mail.to_lowercase();
users::table
.filter(users::email.eq(lower_mail))
.first::<User>(&**conn).ok()
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<User> {
users::table
.filter(users::uuid.eq(uuid))
.first::<User>(&**conn).ok()
}
}

71
src/db/schema.rs Normal file
View file

@ -0,0 +1,71 @@
table! {
ciphers (uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
user_uuid -> Text,
folder_uuid -> Nullable<Text>,
organization_uuid -> Nullable<Text>,
#[sql_name = "type"]
type_ -> Integer,
data -> Text,
favorite -> Bool,
attachments -> Nullable<Binary>,
}
}
table! {
devices (uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
user_uuid -> Text,
name -> Text,
#[sql_name = "type"]
type_ -> Integer,
push_token -> Nullable<Text>,
refresh_token -> Text,
}
}
table! {
folders (uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
user_uuid -> Text,
name -> Text,
}
}
table! {
users (uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
email -> Text,
name -> Text,
password_hash -> Binary,
salt -> Binary,
password_iterations -> Integer,
password_hint -> Nullable<Text>,
key -> Text,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
totp_secret -> Nullable<Text>,
totp_recover -> Nullable<Text>,
security_stamp -> Text,
}
}
joinable!(ciphers -> folders (folder_uuid));
joinable!(ciphers -> users (user_uuid));
joinable!(devices -> users (user_uuid));
joinable!(folders -> users (user_uuid));
allow_tables_to_appear_in_same_query!(
ciphers,
devices,
folders,
users,
);