1
0
mirror of https://github.com/dani-garcia/vaultwarden.git synced 2024-11-10 13:02:41 +01:00

Merge branch 'BlackDex-future-web-vault' into main

This commit is contained in:
Daniel García 2021-07-15 21:51:52 +02:00
commit 96c2416903
No known key found for this signature in database
GPG Key ID: FC8A7D14C3CD543A
18 changed files with 147 additions and 33 deletions

View File

@ -0,0 +1,5 @@
ALTER TABLE organizations
ADD COLUMN private_key TEXT;
ALTER TABLE organizations
ADD COLUMN public_key TEXT;

View File

@ -0,0 +1,5 @@
ALTER TABLE organizations
ADD COLUMN private_key TEXT;
ALTER TABLE organizations
ADD COLUMN public_key TEXT;

View File

@ -0,0 +1,5 @@
ALTER TABLE organizations
ADD COLUMN private_key TEXT;
ALTER TABLE organizations
ADD COLUMN public_key TEXT;

View File

@ -231,7 +231,10 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
err!("Invalid password") err!("Invalid password")
} }
user.set_password(&data.NewMasterPasswordHash, Some("post_rotatekey")); user.set_password(
&data.NewMasterPasswordHash,
Some(vec![String::from("post_rotatekey"), String::from("get_contacts")]),
);
user.akey = data.Key; user.akey = data.Key;
user.save(&conn) user.save(&conn)
} }
@ -320,7 +323,9 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
err!("The cipher is not owned by the user") err!("The cipher is not owned by the user")
} }
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &nt, UpdateType::CipherUpdate)? // Prevent triggering cipher updates via WebSockets by settings UpdateType::None
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &nt, UpdateType::None)?
} }
// Update user data // Update user data
@ -329,7 +334,6 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
user.akey = data.Key; user.akey = data.Key;
user.private_key = Some(data.PrivateKey); user.private_key = Some(data.PrivateKey);
user.reset_security_stamp(); user.reset_security_stamp();
user.reset_stamp_exception();
user.save(&conn) user.save(&conn)
} }

View File

@ -0,0 +1,24 @@
use rocket::Route;
use rocket_contrib::json::Json;
use crate::{api::JsonResult, auth::Headers, db::DbConn};
pub fn routes() -> Vec<Route> {
routes![get_contacts,]
}
/// This endpoint is expected to return at least something.
/// If we return an error message that will trigger error toasts for the user.
/// To prevent this we just return an empty json result with no Data.
/// When this feature is going to be implemented it also needs to return this empty Data
/// instead of throwing an error/4XX unless it really is an error.
#[get("/emergency-access/trusted")]
fn get_contacts(_headers: Headers, _conn: DbConn) -> JsonResult {
debug!("Emergency access is not supported.");
Ok(Json(json!({
"Data": [],
"Object": "list",
"ContinuationToken": null
})))
}

View File

@ -1,5 +1,6 @@
mod accounts; mod accounts;
mod ciphers; mod ciphers;
mod emergency_access;
mod folders; mod folders;
mod organizations; mod organizations;
mod sends; mod sends;
@ -15,6 +16,7 @@ pub fn routes() -> Vec<Route> {
let mut routes = Vec::new(); let mut routes = Vec::new();
routes.append(&mut accounts::routes()); routes.append(&mut accounts::routes());
routes.append(&mut ciphers::routes()); routes.append(&mut ciphers::routes());
routes.append(&mut emergency_access::routes());
routes.append(&mut folders::routes()); routes.append(&mut folders::routes());
routes.append(&mut organizations::routes()); routes.append(&mut organizations::routes());
routes.append(&mut two_factor::routes()); routes.append(&mut two_factor::routes());

View File

@ -51,6 +51,7 @@ pub fn routes() -> Vec<Route> {
get_plans, get_plans,
get_plans_tax_rates, get_plans_tax_rates,
import, import,
post_org_keys,
] ]
} }
@ -61,6 +62,7 @@ struct OrgData {
CollectionName: String, CollectionName: String,
Key: String, Key: String,
Name: String, Name: String,
Keys: Option<OrgKeyData>,
#[serde(rename = "PlanType")] #[serde(rename = "PlanType")]
_PlanType: NumberOrString, // Ignored, always use the same plan _PlanType: NumberOrString, // Ignored, always use the same plan
} }
@ -78,6 +80,13 @@ struct NewCollectionData {
Name: String, Name: String,
} }
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct OrgKeyData {
EncryptedPrivateKey: String,
PublicKey: String,
}
#[post("/organizations", data = "<data>")] #[post("/organizations", data = "<data>")]
fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult { fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult {
if !CONFIG.is_org_creation_allowed(&headers.user.email) { if !CONFIG.is_org_creation_allowed(&headers.user.email) {
@ -85,8 +94,14 @@ fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn
} }
let data: OrgData = data.into_inner().data; let data: OrgData = data.into_inner().data;
let (private_key, public_key) = if data.Keys.is_some() {
let keys: OrgKeyData = data.Keys.unwrap();
(Some(keys.EncryptedPrivateKey), Some(keys.PublicKey))
} else {
(None, None)
};
let org = Organization::new(data.Name, data.BillingEmail); let org = Organization::new(data.Name, data.BillingEmail, private_key, public_key);
let mut user_org = UserOrganization::new(headers.user.uuid, org.uuid.clone()); let mut user_org = UserOrganization::new(headers.user.uuid, org.uuid.clone());
let collection = Collection::new(org.uuid.clone(), data.CollectionName); let collection = Collection::new(org.uuid.clone(), data.CollectionName);
@ -468,6 +483,32 @@ fn get_org_users(org_id: String, _headers: ManagerHeadersLoose, conn: DbConn) ->
})) }))
} }
#[post("/organizations/<org_id>/keys", data = "<data>")]
fn post_org_keys(org_id: String, data: JsonUpcase<OrgKeyData>, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
let data: OrgKeyData = data.into_inner().data;
let mut org = match Organization::find_by_uuid(&org_id, &conn) {
Some(organization) => {
if organization.private_key.is_some() && organization.public_key.is_some() {
err!("Organization Keys already exist")
}
organization
}
None => err!("Can't find organization details"),
};
org.private_key = Some(data.EncryptedPrivateKey);
org.public_key = Some(data.PublicKey);
org.save(&conn)?;
Ok(Json(json!({
"Object": "organizationKeys",
"PublicKey": org.public_key,
"PrivateKey": org.private_key,
})))
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
struct CollectionData { struct CollectionData {

View File

@ -166,8 +166,8 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn
let data = serde_json::from_str::<crate::util::UpCase<SendData>>(&buf)?; let data = serde_json::from_str::<crate::util::UpCase<SendData>>(&buf)?;
enforce_disable_hide_email_policy(&data.data, &headers, &conn)?; enforce_disable_hide_email_policy(&data.data, &headers, &conn)?;
// Get the file length and add an extra 10% to avoid issues // Get the file length and add an extra 5% to avoid issues
const SIZE_110_MB: u64 = 115_343_360; const SIZE_525_MB: u64 = 550_502_400;
let size_limit = match CONFIG.user_attachment_limit() { let size_limit = match CONFIG.user_attachment_limit() {
Some(0) => err!("File uploads are disabled"), Some(0) => err!("File uploads are disabled"),
@ -176,9 +176,9 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn
if left <= 0 { if left <= 0 {
err!("Attachment storage limit reached! Delete some attachments to free up space") err!("Attachment storage limit reached! Delete some attachments to free up space")
} }
std::cmp::Ord::max(left as u64, SIZE_110_MB) std::cmp::Ord::max(left as u64, SIZE_525_MB)
} }
None => SIZE_110_MB, None => SIZE_525_MB,
}; };
// Create the Send // Create the Send

View File

@ -325,8 +325,19 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
_ => err_handler!("Error getting current route for stamp exception"), _ => err_handler!("Error getting current route for stamp exception"),
}; };
// Check if both match, if not this route is not allowed with the current security stamp. // Check if the stamp exception has expired first.
if stamp_exception.route != current_route { // Then, check if the current route matches any of the allowed routes.
// After that check the stamp in exception matches the one in the claims.
if Utc::now().naive_utc().timestamp() > stamp_exception.expire {
// If the stamp exception has been expired remove it from the database.
// This prevents checking this stamp exception for new requests.
let mut user = user;
user.reset_stamp_exception();
if let Err(e) = user.save(&conn) {
error!("Error updating user: {:#?}", e);
}
err_handler!("Stamp exception is expired")
} else if !stamp_exception.routes.contains(&current_route.to_string()) {
err_handler!("Invalid security stamp: Current route and exception route do not match") err_handler!("Invalid security stamp: Current route and exception route do not match")
} else if stamp_exception.security_stamp != claims.sstamp { } else if stamp_exception.security_stamp != claims.sstamp {
err_handler!("Invalid security stamp for matched stamp exception") err_handler!("Invalid security stamp for matched stamp exception")

View File

@ -12,6 +12,8 @@ db_object! {
pub uuid: String, pub uuid: String,
pub name: String, pub name: String,
pub billing_email: String, pub billing_email: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
} }
#[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@ -122,12 +124,13 @@ impl PartialOrd<UserOrgType> for i32 {
/// Local methods /// Local methods
impl Organization { impl Organization {
pub fn new(name: String, billing_email: String) -> Self { pub fn new(name: String, billing_email: String, private_key: Option<String>, public_key: Option<String>) -> Self {
Self { Self {
uuid: crate::util::get_uuid(), uuid: crate::util::get_uuid(),
name, name,
billing_email, billing_email,
private_key,
public_key,
} }
} }
@ -140,14 +143,16 @@ impl Organization {
"MaxCollections": 10, // The value doesn't matter, we don't check server-side "MaxCollections": 10, // The value doesn't matter, we don't check server-side
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side "MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
"Use2fa": true, "Use2fa": true,
"UseDirectory": false, "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
"UseEvents": false, "UseEvents": false, // not supported by us
"UseGroups": false, "UseGroups": false, // not supported by us
"UseTotp": true, "UseTotp": true,
"UsePolicies": true, "UsePolicies": true,
"UseSso": false, // We do not support SSO "UseSso": false, // We do not support SSO
"SelfHost": true, "SelfHost": true,
"UseApi": false, // not supported by us "UseApi": false, // not supported by us
"HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(),
"ResetPasswordEnrolled": false, // not supported by us
"BusinessName": null, "BusinessName": null,
"BusinessAddress1": null, "BusinessAddress1": null,
@ -269,13 +274,15 @@ impl UserOrganization {
"UsersGetPremium": true, "UsersGetPremium": true,
"Use2fa": true, "Use2fa": true,
"UseDirectory": false, "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
"UseEvents": false, "UseEvents": false, // not supported by us
"UseGroups": false, "UseGroups": false, // not supported by us
"UseTotp": true, "UseTotp": true,
"UsePolicies": true, "UsePolicies": true,
"UseApi": false, // not supported by us "UseApi": false, // not supported by us
"SelfHost": true, "SelfHost": true,
"HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(),
"ResetPasswordEnrolled": false, // not supported by us
"SsoBound": false, // We do not support SSO "SsoBound": false, // We do not support SSO
"UseSso": false, // We do not support SSO "UseSso": false, // We do not support SSO
// TODO: Add support for Business Portal // TODO: Add support for Business Portal
@ -293,10 +300,12 @@ impl UserOrganization {
// "AccessReports": false, // "AccessReports": false,
// "ManageAllCollections": false, // "ManageAllCollections": false,
// "ManageAssignedCollections": false, // "ManageAssignedCollections": false,
// "ManageCiphers": false,
// "ManageGroups": false, // "ManageGroups": false,
// "ManagePolicies": false, // "ManagePolicies": false,
// "ManageResetPassword": false,
// "ManageSso": false, // "ManageSso": false,
// "ManageUsers": false // "ManageUsers": false,
// }, // },
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side "MaxStorageGb": 10, // The value doesn't matter, we don't check server-side

View File

@ -1,4 +1,4 @@
use chrono::{NaiveDateTime, Utc}; use chrono::{Duration, NaiveDateTime, Utc};
use serde_json::Value; use serde_json::Value;
use crate::crypto; use crate::crypto;
@ -63,8 +63,9 @@ enum UserStatus {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct UserStampException { pub struct UserStampException {
pub route: String, pub routes: Vec<String>,
pub security_stamp: String, pub security_stamp: String,
pub expire: i64,
} }
/// Local methods /// Local methods
@ -135,9 +136,11 @@ impl User {
/// # Arguments /// # Arguments
/// ///
/// * `password` - A str which contains a hashed version of the users master password. /// * `password` - A str which contains a hashed version of the users master password.
/// * `allow_next_route` - A Option<&str> with the function name of the next allowed (rocket) route. /// * `allow_next_route` - A Option<Vec<String>> with the function names of the next allowed (rocket) routes.
/// These routes are able to use the previous stamp id for the next 2 minutes.
/// After these 2 minutes this stamp will expire.
/// ///
pub fn set_password(&mut self, password: &str, allow_next_route: Option<&str>) { pub fn set_password(&mut self, password: &str, allow_next_route: Option<Vec<String>>) {
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32); self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);
if let Some(route) = allow_next_route { if let Some(route) = allow_next_route {
@ -154,24 +157,20 @@ impl User {
/// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp. /// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp.
/// ///
/// # Arguments /// # Arguments
/// * `route_exception` - A str with the function name of the next allowed (rocket) route. /// * `route_exception` - A Vec<String> with the function names of the next allowed (rocket) routes.
/// These routes are able to use the previous stamp id for the next 2 minutes.
/// After these 2 minutes this stamp will expire.
/// ///
/// ### Future pub fn set_stamp_exception(&mut self, route_exception: Vec<String>) {
/// In the future it could be posible that we need more of these exception routes.
/// In that case we could use an Vec<UserStampException> and add multiple exceptions.
pub fn set_stamp_exception(&mut self, route_exception: &str) {
let stamp_exception = UserStampException { let stamp_exception = UserStampException {
route: route_exception.to_string(), routes: route_exception,
security_stamp: self.security_stamp.to_string(), security_stamp: self.security_stamp.to_string(),
expire: (Utc::now().naive_utc() + Duration::minutes(2)).timestamp(),
}; };
self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default()); self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default());
} }
/// Resets the stamp_exception to prevent re-use of the previous security-stamp /// Resets the stamp_exception to prevent re-use of the previous security-stamp
///
/// ### Future
/// In the future it could be posible that we need more of these exception routes.
/// In that case we could use an Vec<UserStampException> and add multiple exceptions.
pub fn reset_stamp_exception(&mut self) { pub fn reset_stamp_exception(&mut self) {
self.stamp_exception = None; self.stamp_exception = None;
} }

View File

@ -100,6 +100,8 @@ table! {
uuid -> Text, uuid -> Text,
name -> Text, name -> Text,
billing_email -> Text, billing_email -> Text,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
} }
} }

View File

@ -100,6 +100,8 @@ table! {
uuid -> Text, uuid -> Text,
name -> Text, name -> Text,
billing_email -> Text, billing_email -> Text,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
} }
} }

View File

@ -100,6 +100,8 @@ table! {
uuid -> Text, uuid -> Text,
name -> Text, name -> Text,
billing_email -> Text, billing_email -> Text,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
} }
} }

View File

@ -174,6 +174,9 @@ fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
"Message": msg, "Message": msg,
"Object": "error" "Object": "error"
}, },
"ExceptionMessage": null,
"ExceptionStackTrace": null,
"InnerExceptionMessage": null,
"Object": "error" "Object": "error"
}); });
_serialize(&json, "") _serialize(&json, "")