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:
commit
5cd40c63ed
172 changed files with 17903 additions and 0 deletions
60
src/db/mod.rs
Normal file
60
src/db/mod.rs
Normal 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
112
src/db/models/cipher.rs
Normal 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
117
src/db/models/device.rs
Normal 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
83
src/db/models/folder.rs
Normal 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
9
src/db/models/mod.rs
Normal 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
159
src/db/models/user.rs
Normal 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
71
src/db/schema.rs
Normal 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,
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue