Merge branch 'rocket-0.4'

This commit is contained in:
Daniel García 2018-12-13 17:49:55 +01:00
commit a1272c7190
No known key found for this signature in database
GPG key ID: FC8A7D14C3CD543A
44 changed files with 1357 additions and 1968 deletions

11
.env
View file

@ -18,6 +18,17 @@
# WEBSOCKET_ADDRESS=0.0.0.0
# WEBSOCKET_PORT=3012
## Enable extended logging
## This shows timestamps and allows logging to file and to syslog
### To enable logging to file, use the LOG_FILE env variable
### To enable syslog, you need to compile with `cargo build --features=enable_syslog'
# EXTENDED_LOGGING=true
## Logging to file
## This requires extended logging
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
# LOG_FILE=/path/to/log
## Controls if new users can register
# SIGNUPS_ALLOWED=true

View file

@ -5,3 +5,5 @@ dist: trusty # so we get a VM with higher specs
cache: cargo
rust:
- nightly
script:
- cargo build --verbose --all-features

853
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,21 +2,24 @@
name = "bitwarden_rs"
version = "1.0.0"
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
edition = "2018"
[features]
enable_syslog = ["syslog", "fern/syslog-4"]
[dependencies]
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
rocket = { version = "0.3.17", features = ["tls"] }
rocket_codegen = "0.3.17"
rocket_contrib = "0.3.17"
rocket = { version = "0.4.0", features = ["tls"], default-features = false }
rocket_contrib = "0.4.0"
# HTTP client
reqwest = "0.9.2"
reqwest = "0.9.5"
# multipart/form-data support
multipart = "0.15.3"
multipart = "0.15.4"
# WebSockets library
ws = "0.7.8"
ws = "0.7.9"
# MessagePack library
rmpv = "0.4.0"
@ -25,9 +28,14 @@ rmpv = "0.4.0"
chashmap = "2.2.0"
# A generic serialization/deserialization framework
serde = "1.0.79"
serde_derive = "1.0.79"
serde_json = "1.0.31"
serde = "1.0.82"
serde_derive = "1.0.82"
serde_json = "1.0.33"
# Logging
log = "0.4.6"
fern = "0.5.7"
syslog = { version = "4.0.1", optional = true }
# A safe, extensible ORM and Query builder
diesel = { version = "1.3.3", features = ["sqlite", "chrono", "r2d2"] }
@ -37,7 +45,7 @@ diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
libsqlite3-sys = { version = "0.9.3", features = ["bundled"] }
# Crypto library
ring = { version = "= 0.11.0", features = ["rsa_signing"] }
ring = { version = "0.13.5", features = ["rsa_signing"] }
# UUID generation
uuid = { version = "0.7.1", features = ["v4"] }
@ -52,7 +60,7 @@ oath = "0.10.2"
data-encoding = "2.1.1"
# JWT library
jsonwebtoken = "= 4.0.1"
jsonwebtoken = "5.0.1"
# U2F library
u2f = "0.1.2"
@ -64,7 +72,7 @@ yubico = { version = "=0.4.0", features = ["online"], default-features = false }
dotenv = { version = "0.13.0", default-features = false }
# Lazy static macro
lazy_static = "1.1.0"
lazy_static = { version = "1.2.0", features = ["nightly"] }
# Numerical libraries
num-traits = "0.2.6"
@ -73,20 +81,21 @@ num-derive = "0.2.3"
# Email libraries
lettre = "0.9.0"
lettre_email = "0.9.0"
native-tls = "0.2.1"
native-tls = "0.2.2"
# Number encoding library
byteorder = "1.2.6"
byteorder = "1.2.7"
[patch.crates-io]
# Make jwt use ring 0.11, to match rocket
jsonwebtoken = { path = "libs/jsonwebtoken" }
# Add support for Timestamp type
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
# Use new native_tls version 0.2
lettre = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
lettre_email = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
# 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' }
u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '75b9fa5afb4c5' }
# Allows optional libusb support
yubico = { git = 'https://github.com/dani-garcia/yubico-rs' }

View file

@ -2,9 +2,9 @@
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM node:8-alpine as vault
FROM node:10-alpine as vault
ENV VAULT_VERSION "v2.5.0"
ENV VAULT_VERSION "v2.6.1"
ENV URL "https://github.com/bitwarden/web.git"
@ -41,7 +41,6 @@ WORKDIR /app
# Copies over *only* your manifests and vendored dependencies
COPY ./Cargo.* ./
COPY ./libs ./libs
COPY ./rust-toolchain ./rust-toolchain
# Builds your dependencies and removes the
@ -54,6 +53,9 @@ RUN find . -not -path "./target*" -delete
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Make sure that we actually build the project
RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --release
@ -64,6 +66,7 @@ RUN cargo build --release
FROM debian:stretch-slim
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
# Install needed libraries

View file

@ -2,9 +2,9 @@
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM node:8-alpine as vault
FROM node:10-alpine as vault
ENV VAULT_VERSION "v2.5.0"
ENV VAULT_VERSION "v2.6.1"
ENV URL "https://github.com/bitwarden/web.git"
@ -69,6 +69,7 @@ RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
FROM balenalib/aarch64-debian:stretch
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
RUN [ "cross-build-start" ]
@ -95,4 +96,4 @@ COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
# Configures the startup!
CMD ./bitwarden_rs
CMD ./bitwarden_rs

View file

@ -2,9 +2,9 @@
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM node:8-alpine as vault
FROM node:10-alpine as vault
ENV VAULT_VERSION "v2.5.0"
ENV VAULT_VERSION "v2.6.1"
ENV URL "https://github.com/bitwarden/web.git"
@ -26,7 +26,7 @@ RUN npm run dist \
########################## BUILD IMAGE ##########################
# Musl build image for statically compiled binary
FROM clux/muslrust:nightly-2018-10-03 as build
FROM clux/muslrust:nightly-2018-11-30 as build
ENV USER "root"
@ -45,6 +45,7 @@ RUN cargo build --release
FROM alpine:3.8
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
ENV SSL_CERT_DIR=/etc/ssl/certs

View file

@ -2,9 +2,9 @@
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM node:8-alpine as vault
FROM node:10-alpine as vault
ENV VAULT_VERSION "v2.5.0"
ENV VAULT_VERSION "v2.6.1"
ENV URL "https://github.com/bitwarden/web.git"
@ -69,6 +69,7 @@ RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
FROM balenalib/armv7hf-debian:stretch
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
RUN [ "cross-build-start" ]

View file

@ -1,20 +0,0 @@
[package]
name = "jsonwebtoken"
version = "4.0.1"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
license = "MIT"
readme = "README.md"
description = "Create and parse JWT in a strongly typed way."
homepage = "https://github.com/Keats/rust-jwt"
repository = "https://github.com/Keats/rust-jwt"
keywords = ["jwt", "web", "api", "token", "json"]
[dependencies]
error-chain = { version = "0.11", default-features = false }
serde_json = "1.0"
serde_derive = "1.0"
serde = "1.0"
ring = { version = "0.11.0", features = ["rsa_signing", "dev_urandom_fallback"] }
base64 = "0.9"
untrusted = "0.5"
chrono = "0.4"

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Vincent Prouillet
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,120 +0,0 @@
use std::sync::Arc;
use base64;
use ring::{rand, digest, hmac, signature};
use ring::constant_time::verify_slices_are_equal;
use untrusted;
use errors::{Result, ErrorKind};
/// The algorithms supported for signing/verifying
#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum Algorithm {
/// HMAC using SHA-256
HS256,
/// HMAC using SHA-384
HS384,
/// HMAC using SHA-512
HS512,
/// RSASSA-PKCS1-v1_5 using SHA-256
RS256,
/// RSASSA-PKCS1-v1_5 using SHA-384
RS384,
/// RSASSA-PKCS1-v1_5 using SHA-512
RS512,
}
/// The actual HS signing + encoding
fn sign_hmac(alg: &'static digest::Algorithm, key: &[u8], signing_input: &str) -> Result<String> {
let signing_key = hmac::SigningKey::new(alg, key);
let digest = hmac::sign(&signing_key, signing_input.as_bytes());
Ok(
base64::encode_config::<hmac::Signature>(&digest, base64::URL_SAFE_NO_PAD)
)
}
/// The actual RSA signing + encoding
/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html
fn sign_rsa(alg: Algorithm, key: &[u8], signing_input: &str) -> Result<String> {
let ring_alg = match alg {
Algorithm::RS256 => &signature::RSA_PKCS1_SHA256,
Algorithm::RS384 => &signature::RSA_PKCS1_SHA384,
Algorithm::RS512 => &signature::RSA_PKCS1_SHA512,
_ => unreachable!(),
};
let key_pair = Arc::new(
signature::RSAKeyPair::from_der(untrusted::Input::from(key))
.map_err(|_| ErrorKind::InvalidKey)?
);
let mut signing_state = signature::RSASigningState::new(key_pair)
.map_err(|_| ErrorKind::InvalidKey)?;
let mut signature = vec![0; signing_state.key_pair().public_modulus_len()];
let rng = rand::SystemRandom::new();
signing_state.sign(ring_alg, &rng, signing_input.as_bytes(), &mut signature)
.map_err(|_| ErrorKind::InvalidKey)?;
Ok(
base64::encode_config::<[u8]>(&signature, base64::URL_SAFE_NO_PAD)
)
}
/// Take the payload of a JWT, sign it using the algorithm given and return
/// the base64 url safe encoded of the result.
///
/// Only use this function if you want to do something other than JWT.
pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result<String> {
match algorithm {
Algorithm::HS256 => sign_hmac(&digest::SHA256, key, signing_input),
Algorithm::HS384 => sign_hmac(&digest::SHA384, key, signing_input),
Algorithm::HS512 => sign_hmac(&digest::SHA512, key, signing_input),
Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => sign_rsa(algorithm, key, signing_input),
// TODO: if PKCS1 is made prublic, remove the line above and uncomment below
// Algorithm::RS256 => sign_rsa(&signature::RSA_PKCS1_SHA256, key, signing_input),
// Algorithm::RS384 => sign_rsa(&signature::RSA_PKCS1_SHA384, key, signing_input),
// Algorithm::RS512 => sign_rsa(&signature::RSA_PKCS1_SHA512, key, signing_input),
}
}
/// See Ring RSA docs for more details
fn verify_rsa(alg: &signature::RSAParameters, signature: &str, signing_input: &str, key: &[u8]) -> Result<bool> {
let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?;
let public_key_der = untrusted::Input::from(key);
let message = untrusted::Input::from(signing_input.as_bytes());
let expected_signature = untrusted::Input::from(signature_bytes.as_slice());
let res = signature::verify(alg, public_key_der, message, expected_signature);
Ok(res.is_ok())
}
/// Compares the signature given with a re-computed signature for HMAC or using the public key
/// for RSA.
///
/// Only use this function if you want to do something other than JWT.
///
/// `signature` is the signature part of a jwt (text after the second '.')
///
/// `signing_input` is base64(header) + "." + base64(claims)
pub fn verify(signature: &str, signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result<bool> {
match algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
// we just re-sign the data with the key and compare if they are equal
let signed = sign(signing_input, key, algorithm)?;
Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok())
},
Algorithm::RS256 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA256, signature, signing_input, key),
Algorithm::RS384 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA384, signature, signing_input, key),
Algorithm::RS512 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key),
}
}
impl Default for Algorithm {
fn default() -> Self {
Algorithm::HS256
}
}

View file

@ -1,68 +0,0 @@
use base64;
use serde_json;
use ring;
error_chain! {
errors {
/// When a token doesn't have a valid JWT shape
InvalidToken {
description("invalid token")
display("Invalid token")
}
/// When the signature doesn't match
InvalidSignature {
description("invalid signature")
display("Invalid signature")
}
/// When the secret given is not a valid RSA key
InvalidKey {
description("invalid key")
display("Invalid Key")
}
// Validation error
/// When a tokens `exp` claim indicates that it has expired
ExpiredSignature {
description("expired signature")
display("Expired Signature")
}
/// When a tokens `iss` claim does not match the expected issuer
InvalidIssuer {
description("invalid issuer")
display("Invalid Issuer")
}
/// When a tokens `aud` claim does not match one of the expected audience values
InvalidAudience {
description("invalid audience")
display("Invalid Audience")
}
/// When a tokens `aud` claim does not match one of the expected audience values
InvalidSubject {
description("invalid subject")
display("Invalid Subject")
}
/// When a tokens `iat` claim is in the future
InvalidIssuedAt {
description("invalid issued at")
display("Invalid Issued At")
}
/// When a tokens nbf claim represents a time in the future
ImmatureSignature {
description("immature signature")
display("Immature Signature")
}
/// When the algorithm in the header doesn't match the one passed to `decode`
InvalidAlgorithm {
description("Invalid algorithm")
display("Invalid Algorithm")
}
}
foreign_links {
Unspecified(ring::error::Unspecified) #[doc = "An error happened while signing/verifying a token with RSA"];
Base64(base64::DecodeError) #[doc = "An error happened while decoding some base64 text"];
Json(serde_json::Error) #[doc = "An error happened while serializing/deserializing JSON"];
Utf8(::std::string::FromUtf8Error) #[doc = "An error happened while trying to convert the result of base64 decoding to a String"];
}
}

View file

@ -1,64 +0,0 @@
use crypto::Algorithm;
/// A basic JWT header, the alg defaults to HS256 and typ is automatically
/// set to `JWT`. All the other fields are optional.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Header {
/// The type of JWS: it can only be "JWT" here
///
/// Defined in [RFC7515#4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9).
#[serde(skip_serializing_if = "Option::is_none")]
pub typ: Option<String>,
/// The algorithm used
///
/// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1).
pub alg: Algorithm,
/// Content type
///
/// Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2).
#[serde(skip_serializing_if = "Option::is_none")]
pub cty: Option<String>,
/// JSON Key URL
///
/// Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2).
#[serde(skip_serializing_if = "Option::is_none")]
pub jku: Option<String>,
/// Key ID
///
/// Defined in [RFC7515#4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4).
#[serde(skip_serializing_if = "Option::is_none")]
pub kid: Option<String>,
/// X.509 URL
///
/// Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5).
#[serde(skip_serializing_if = "Option::is_none")]
pub x5u: Option<String>,
/// X.509 certificate thumbprint
///
/// Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7).
#[serde(skip_serializing_if = "Option::is_none")]
pub x5t: Option<String>,
}
impl Header {
/// Returns a JWT header with the algorithm given
pub fn new(algorithm: Algorithm) -> Header {
Header {
typ: Some("JWT".to_string()),
alg: algorithm,
cty: None,
jku: None,
kid: None,
x5u: None,
x5t: None,
}
}
}
impl Default for Header {
/// Returns a JWT header using the default Algorithm, HS256
fn default() -> Self {
Header::new(Algorithm::default())
}
}

View file

@ -1,142 +0,0 @@
//! Create and parses JWT (JSON Web Tokens)
//!
//! Documentation: [stable](https://docs.rs/jsonwebtoken/)
#![recursion_limit = "300"]
#![deny(missing_docs)]
#![allow(unused_doc_comments)]
#![allow(renamed_and_removed_lints)]
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate serde;
extern crate base64;
extern crate ring;
extern crate untrusted;
extern crate chrono;
/// All the errors, generated using error-chain
pub mod errors;
mod header;
mod crypto;
mod serialization;
mod validation;
pub use header::Header;
pub use crypto::{
Algorithm,
sign,
verify,
};
pub use validation::Validation;
pub use serialization::TokenData;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use errors::{Result, ErrorKind};
use serialization::{from_jwt_part, from_jwt_part_claims, to_jwt_part};
use validation::{validate};
/// Encode the header and claims given and sign the payload using the algorithm from the header and the key
///
/// ```rust,ignore
/// #[macro_use]
/// extern crate serde_derive;
/// use jsonwebtoken::{encode, Algorithm, Header};
///
/// /// #[derive(Debug, Serialize, Deserialize)]
/// struct Claims {
/// sub: String,
/// company: String
/// }
///
/// let my_claims = Claims {
/// sub: "b@b.com".to_owned(),
/// company: "ACME".to_owned()
/// };
///
/// // my_claims is a struct that implements Serialize
/// // This will create a JWT using HS256 as algorithm
/// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap();
/// ```
pub fn encode<T: Serialize>(header: &Header, claims: &T, key: &[u8]) -> Result<String> {
let encoded_header = to_jwt_part(&header)?;
let encoded_claims = to_jwt_part(&claims)?;
let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join(".");
let signature = sign(&*signing_input, key.as_ref(), header.alg)?;
Ok([signing_input, signature].join("."))
}
/// Used in decode: takes the result of a rsplit and ensure we only get 2 parts
/// Errors if we don't
macro_rules! expect_two {
($iter:expr) => {{
let mut i = $iter;
match (i.next(), i.next(), i.next()) {
(Some(first), Some(second), None) => (first, second),
_ => return Err(ErrorKind::InvalidToken.into())
}
}}
}
/// Decode a token into a struct containing 2 fields: `claims` and `header`.
///
/// If the token or its signature is invalid or the claims fail validation, it will return an error.
///
/// ```rust,ignore
/// #[macro_use]
/// extern crate serde_derive;
/// use jsonwebtoken::{decode, Validation, Algorithm};
///
/// #[derive(Debug, Serialize, Deserialize)]
/// struct Claims {
/// sub: String,
/// company: String
/// }
///
/// let token = "a.jwt.token".to_string();
/// // Claims is a struct that implements Deserialize
/// let token_data = decode::<Claims>(&token, "secret", &Validation::new(Algorithm::HS256));
/// ```
pub fn decode<T: DeserializeOwned>(token: &str, key: &[u8], validation: &Validation) -> Result<TokenData<T>> {
let (signature, signing_input) = expect_two!(token.rsplitn(2, '.'));
let (claims, header) = expect_two!(signing_input.rsplitn(2, '.'));
let header: Header = from_jwt_part(header)?;
if !verify(signature, signing_input, key, header.alg)? {
return Err(ErrorKind::InvalidSignature.into());
}
if !validation.algorithms.contains(&header.alg) {
return Err(ErrorKind::InvalidAlgorithm.into());
}
let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?;
validate(&claims_map, validation)?;
Ok(TokenData { header: header, claims: decoded_claims })
}
/// Decode a token and return the Header. This is not doing any kind of validation: it is meant to be
/// used when you don't know which `alg` the token is using and want to find out.
///
/// If the token has an invalid format, it will return an error.
///
/// ```rust,ignore
/// use jsonwebtoken::decode_header;
///
/// let token = "a.jwt.token".to_string();
/// let header = decode_header(&token);
/// ```
pub fn decode_header(token: &str) -> Result<Header> {
let (_, signing_input) = expect_two!(token.rsplitn(2, '.'));
let (_, header) = expect_two!(signing_input.rsplitn(2, '.'));
from_jwt_part(header)
}

View file

@ -1,42 +0,0 @@
use base64;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use serde_json::{from_str, to_string, Value};
use serde_json::map::Map;
use errors::{Result};
use header::Header;
/// The return type of a successful call to decode
#[derive(Debug)]
pub struct TokenData<T> {
/// The decoded JWT header
pub header: Header,
/// The decoded JWT claims
pub claims: T
}
/// Serializes to JSON and encodes to base64
pub fn to_jwt_part<T: Serialize>(input: &T) -> Result<String> {
let encoded = to_string(input)?;
Ok(base64::encode_config(encoded.as_bytes(), base64::URL_SAFE_NO_PAD))
}
/// Decodes from base64 and deserializes from JSON to a struct
pub fn from_jwt_part<B: AsRef<str>, T: DeserializeOwned>(encoded: B) -> Result<T> {
let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?;
let s = String::from_utf8(decoded)?;
Ok(from_str(&s)?)
}
/// Decodes from base64 and deserializes from JSON to a struct AND a hashmap
pub fn from_jwt_part_claims<B: AsRef<str>, T: DeserializeOwned>(encoded: B) -> Result<(T, Map<String, Value>)> {
let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?;
let s = String::from_utf8(decoded)?;
let claims: T = from_str(&s)?;
let map: Map<_,_> = from_str(&s)?;
Ok((claims, map))
}

View file

@ -1,377 +0,0 @@
use chrono::Utc;
use serde::ser::Serialize;
use serde_json::{Value, from_value, to_value};
use serde_json::map::Map;
use errors::{Result, ErrorKind};
use crypto::Algorithm;
/// Contains the various validations that are applied after decoding a token.
///
/// All time validation happen on UTC timestamps.
///
/// ```rust
/// use jsonwebtoken::Validation;
///
/// // Default value
/// let validation = Validation::default();
///
/// // Changing one parameter
/// let mut validation = Validation {leeway: 60, ..Default::default()};
///
/// // Setting audience
/// let mut validation = Validation::default();
/// validation.set_audience(&"Me"); // string
/// validation.set_audience(&["Me", "You"]); // array of strings
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct Validation {
/// Add some leeway (in seconds) to the `exp`, `iat` and `nbf` validation to
/// account for clock skew.
///
/// Defaults to `0`.
pub leeway: i64,
/// Whether to validate the `exp` field.
///
/// It will return an error if the time in the `exp` field is past.
///
/// Defaults to `true`.
pub validate_exp: bool,
/// Whether to validate the `iat` field.
///
/// It will return an error if the time in the `iat` field is in the future.
///
/// Defaults to `true`.
pub validate_iat: bool,
/// Whether to validate the `nbf` field.
///
/// It will return an error if the current timestamp is before the time in the `nbf` field.
///
/// Defaults to `true`.
pub validate_nbf: bool,
/// If it contains a value, the validation will check that the `aud` field is the same as the
/// one provided and will error otherwise.
/// Since `aud` can be either a String or a Vec<String> in the JWT spec, you will need to use
/// the [set_audience](struct.Validation.html#method.set_audience) method to set it.
///
/// Defaults to `None`.
pub aud: Option<Value>,
/// If it contains a value, the validation will check that the `iss` field is the same as the
/// one provided and will error otherwise.
///
/// Defaults to `None`.
pub iss: Option<String>,
/// If it contains a value, the validation will check that the `sub` field is the same as the
/// one provided and will error otherwise.
///
/// Defaults to `None`.
pub sub: Option<String>,
/// If it contains a value, the validation will check that the `alg` of the header is contained
/// in the ones provided and will error otherwise.
///
/// Defaults to `vec![Algorithm::HS256]`.
pub algorithms: Vec<Algorithm>,
}
impl Validation {
/// Create a default validation setup allowing the given alg
pub fn new(alg: Algorithm) -> Validation {
let mut validation = Validation::default();
validation.algorithms = vec![alg];
validation
}
/// Since `aud` can be either a String or an array of String in the JWT spec, this method will take
/// care of serializing the value.
pub fn set_audience<T: Serialize>(&mut self, audience: &T) {
self.aud = Some(to_value(audience).unwrap());
}
}
impl Default for Validation {
fn default() -> Validation {
Validation {
leeway: 0,
validate_exp: true,
validate_iat: true,
validate_nbf: true,
iss: None,
sub: None,
aud: None,
algorithms: vec![Algorithm::HS256],
}
}
}
pub fn validate(claims: &Map<String, Value>, options: &Validation) -> Result<()> {
let now = Utc::now().timestamp();
if let Some(iat) = claims.get("iat") {
if options.validate_iat && from_value::<i64>(iat.clone())? > now + options.leeway {
return Err(ErrorKind::InvalidIssuedAt.into());
}
}
if let Some(exp) = claims.get("exp") {
if options.validate_exp && from_value::<i64>(exp.clone())? < now - options.leeway {
return Err(ErrorKind::ExpiredSignature.into());
}
}
if let Some(nbf) = claims.get("nbf") {
if options.validate_nbf && from_value::<i64>(nbf.clone())? > now + options.leeway {
return Err(ErrorKind::ImmatureSignature.into());
}
}
if let Some(iss) = claims.get("iss") {
if let Some(ref correct_iss) = options.iss {
if from_value::<String>(iss.clone())? != *correct_iss {
return Err(ErrorKind::InvalidIssuer.into());
}
}
}
if let Some(sub) = claims.get("sub") {
if let Some(ref correct_sub) = options.sub {
if from_value::<String>(sub.clone())? != *correct_sub {
return Err(ErrorKind::InvalidSubject.into());
}
}
}
if let Some(aud) = claims.get("aud") {
if let Some(ref correct_aud) = options.aud {
if aud != correct_aud {
return Err(ErrorKind::InvalidAudience.into());
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use serde_json::{to_value};
use serde_json::map::Map;
use chrono::Utc;
use super::{validate, Validation};
use errors::ErrorKind;
#[test]
fn iat_in_past_ok() {
let mut claims = Map::new();
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_ok());
}
#[test]
fn iat_in_future_fails() {
let mut claims = Map::new();
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidIssuedAt => (),
_ => assert!(false),
};
}
#[test]
fn iat_in_future_but_in_leeway_ok() {
let mut claims = Map::new();
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() + 50).unwrap());
let validation = Validation {
leeway: 1000 * 60,
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn exp_in_future_ok() {
let mut claims = Map::new();
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() + 10000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_ok());
}
#[test]
fn exp_in_past_fails() {
let mut claims = Map::new();
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 100000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::ExpiredSignature => (),
_ => assert!(false),
};
}
#[test]
fn exp_in_past_but_in_leeway_ok() {
let mut claims = Map::new();
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 500).unwrap());
let validation = Validation {
leeway: 1000 * 60,
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn nbf_in_past_ok() {
let mut claims = Map::new();
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_ok());
}
#[test]
fn nbf_in_future_fails() {
let mut claims = Map::new();
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::ImmatureSignature => (),
_ => assert!(false),
};
}
#[test]
fn nbf_in_future_but_in_leeway_ok() {
let mut claims = Map::new();
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 500).unwrap());
let validation = Validation {
leeway: 1000 * 60,
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn iss_ok() {
let mut claims = Map::new();
claims.insert("iss".to_string(), to_value("Keats").unwrap());
let validation = Validation {
iss: Some("Keats".to_string()),
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn iss_not_matching_fails() {
let mut claims = Map::new();
claims.insert("iss".to_string(), to_value("Hacked").unwrap());
let validation = Validation {
iss: Some("Keats".to_string()),
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidIssuer => (),
_ => assert!(false),
};
}
#[test]
fn sub_ok() {
let mut claims = Map::new();
claims.insert("sub".to_string(), to_value("Keats").unwrap());
let validation = Validation {
sub: Some("Keats".to_string()),
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn sub_not_matching_fails() {
let mut claims = Map::new();
claims.insert("sub".to_string(), to_value("Hacked").unwrap());
let validation = Validation {
sub: Some("Keats".to_string()),
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidSubject => (),
_ => assert!(false),
};
}
#[test]
fn aud_string_ok() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
let mut validation = Validation::default();
validation.set_audience(&"Everyone");
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn aud_array_of_string_ok() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value(["UserA", "UserB"]).unwrap());
let mut validation = Validation::default();
validation.set_audience(&["UserA", "UserB"]);
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn aud_type_mismatch_fails() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
let mut validation = Validation::default();
validation.set_audience(&["UserA", "UserB"]);
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidAudience => (),
_ => assert!(false),
};
}
#[test]
fn aud_correct_type_not_matching_fails() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
let mut validation = Validation::default();
validation.set_audience(&"None");
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidAudience => (),
_ => assert!(false),
};
}
}

View file

@ -0,0 +1,3 @@
ALTER TABLE attachments
ADD COLUMN
key TEXT;

View file

@ -1 +1 @@
nightly-2018-10-03
nightly-2018-12-01

View file

@ -1,13 +1,37 @@
use rocket_contrib::Json;
use rocket_contrib::json::Json;
use db::DbConn;
use db::models::*;
use crate::db::models::*;
use crate::db::DbConn;
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString};
use auth::Headers;
use mail;
use crate::api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData, UpdateType, WebSocketUsers};
use crate::auth::Headers;
use crate::mail;
use CONFIG;
use crate::CONFIG;
use rocket::{Route, State};
pub fn routes() -> Vec<Route> {
routes![
register,
profile,
put_profile,
post_profile,
get_public_keys,
post_keys,
post_password,
post_kdf,
post_rotatekey,
post_sstamp,
post_email_token,
post_email,
delete_account,
post_delete_account,
revision_date,
password_hint,
prelogin,
]
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
@ -33,23 +57,22 @@ struct KeysData {
fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
let data: RegisterData = data.into_inner().data;
let mut user = match User::find_by_mail(&data.Email, &conn) {
Some(mut user) => {
Some(user) => {
if Invitation::take(&data.Email, &conn) {
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
user_org.status = UserOrgStatus::Accepted as i32;
if user_org.save(&conn).is_err() {
err!("Failed to accept user to organization")
}
};
}
user
} else if CONFIG.signups_allowed {
err!("Account with this email already exists")
err!("Account with this email already exists")
} else {
err!("Registration not allowed")
err!("Registration not allowed")
}
},
}
None => {
if CONFIG.signups_allowed || Invitation::take(&data.Email, &conn) {
User::new(data.Email)
@ -86,7 +109,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save user")
Err(_) => err!("Failed to save user"),
}
}
@ -99,7 +122,7 @@ fn profile(headers: Headers, conn: DbConn) -> JsonResult {
#[allow(non_snake_case)]
struct ProfileData {
#[serde(rename = "Culture")]
_Culture: String, // Ignored, always use en-US
_Culture: String, // Ignored, always use en-US
MasterPasswordHint: Option<String>,
Name: String,
}
@ -122,7 +145,7 @@ fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -
};
match user.save(&conn) {
Ok(()) => Ok(Json(user.to_json(&conn))),
Err(_) => err!("Failed to save user profile")
Err(_) => err!("Failed to save user profile"),
}
}
@ -130,7 +153,7 @@ fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -
fn get_public_keys(uuid: String, _headers: Headers, conn: DbConn) -> JsonResult {
let user = match User::find_by_uuid(&uuid, &conn) {
Some(user) => user,
None => err!("User doesn't exist")
None => err!("User doesn't exist"),
};
Ok(Json(json!({
@ -151,12 +174,10 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
match user.save(&conn) {
Ok(()) => Ok(Json(user.to_json(&conn))),
Err(_) => err!("Failed to save the user's keys")
Err(_) => err!("Failed to save the user's keys"),
}
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct ChangePassData {
@ -178,7 +199,7 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
user.key = data.Key;
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save password")
Err(_) => err!("Failed to save password"),
}
}
@ -208,10 +229,86 @@ fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) ->
user.key = data.Key;
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save password settings")
Err(_) => err!("Failed to save password settings"),
}
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct UpdateFolderData {
Id: String,
Name: String,
}
use super::ciphers::CipherData;
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct KeyData {
Ciphers: Vec<CipherData>,
Folders: Vec<UpdateFolderData>,
Key: String,
PrivateKey: String,
MasterPasswordHash: String,
}
#[post("/accounts/key", data = "<data>")]
fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
let data: KeyData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password")
}
let user_uuid = &headers.user.uuid;
// Update folder data
for folder_data in data.Folders {
let mut saved_folder = match Folder::find_by_uuid(&folder_data.Id, &conn) {
Some(folder) => folder,
None => err!("Folder doesn't exist"),
};
if &saved_folder.user_uuid != user_uuid {
err!("The folder is not owned by the user")
}
saved_folder.name = folder_data.Name;
if saved_folder.save(&conn).is_err() {
err!("Failed to save folder")
}
}
// Update cipher data
use super::ciphers::update_cipher_from_data;
for cipher_data in data.Ciphers {
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist"),
};
if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid {
err!("The cipher is not owned by the user")
}
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &ws, UpdateType::SyncCipherUpdate)?
}
// Update user data
let mut user = headers.user;
user.key = data.Key;
user.private_key = Some(data.PrivateKey);
user.reset_security_stamp();
if user.save(&conn).is_err() {
err!("Failed modify user key");
}
Ok(())
}
#[post("/accounts/security-stamp", data = "<data>")]
fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: PasswordData = data.into_inner().data;
@ -224,7 +321,7 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
user.reset_security_stamp();
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to reset security stamp")
Err(_) => err!("Failed to reset security stamp"),
}
}
@ -255,7 +352,7 @@ fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: Db
struct ChangeEmailData {
MasterPasswordHash: String,
NewEmail: String,
Key: String,
NewMasterPasswordHash: String,
#[serde(rename = "Token")]
@ -276,13 +373,13 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
}
user.email = data.NewEmail;
user.set_password(&data.NewMasterPasswordHash);
user.key = data.Key;
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save email address")
Err(_) => err!("Failed to save email address"),
}
}
@ -299,10 +396,10 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
if !user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password")
}
match user.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed deleting user account, are you the only owner of some organization?")
Err(_) => err!("Failed deleting user account, are you the only owner of some organization?"),
}
}

View file

@ -1,35 +1,76 @@
use std::collections::{HashSet, HashMap};
use std::path::Path;
use std::collections::HashSet;
use rocket::State;
use rocket::Data;
use rocket::http::ContentType;
use rocket::{request::Form, Data, Route, State};
use rocket_contrib::{Json, Value};
use rocket_contrib::json::Json;
use serde_json::Value;
use multipart::server::{Multipart, SaveResult};
use multipart::server::save::SavedData;
use multipart::server::{Multipart, SaveResult};
use data_encoding::HEXLOWER;
use db::DbConn;
use db::models::*;
use crate::db::models::*;
use crate::db::DbConn;
use crypto;
use crate::crypto;
use api::{self, PasswordData, JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
use auth::Headers;
use crate::api::{self, EmptyResult, JsonResult, JsonUpcase, PasswordData, UpdateType, WebSocketUsers};
use crate::auth::Headers;
use CONFIG;
use crate::CONFIG;
#[derive(FromForm)]
#[allow(non_snake_case)]
struct SyncData {
excludeDomains: bool,
pub fn routes() -> Vec<Route> {
routes![
sync,
get_ciphers,
get_cipher,
get_cipher_admin,
get_cipher_details,
post_ciphers,
put_cipher_admin,
post_ciphers_admin,
post_ciphers_create,
post_ciphers_import,
post_attachment,
post_attachment_admin,
post_attachment_share,
delete_attachment_post,
delete_attachment_post_admin,
delete_attachment,
delete_attachment_admin,
post_cipher_admin,
post_cipher_share,
put_cipher_share,
put_cipher_share_seleted,
post_cipher,
put_cipher,
delete_cipher_post,
delete_cipher_post_admin,
delete_cipher,
delete_cipher_admin,
delete_cipher_selected,
delete_cipher_selected_post,
delete_all,
move_cipher_selected,
move_cipher_selected_put,
post_collections_update,
post_collections_admin,
put_collections_admin,
]
}
#[get("/sync?<data>")]
fn sync(data: SyncData, headers: Headers, conn: DbConn) -> JsonResult {
#[derive(FromForm, Default)]
struct SyncData {
#[form(field = "excludeDomains")]
exclude_domains: bool, // Default: 'false'
}
#[get("/sync?<data..>")]
fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
let user_json = headers.user.to_json(&conn);
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
@ -41,7 +82,7 @@ fn sync(data: SyncData, headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
let domains_json = if data.excludeDomains { Value::Null } else { api::core::get_eq_domains(headers).unwrap().into_inner() };
let domains_json = if data.exclude_domains { Value::Null } else { api::core::get_eq_domains(headers).unwrap().into_inner() };
Ok(Json(json!({
"Profile": user_json,
@ -53,14 +94,6 @@ fn sync(data: SyncData, headers: Headers, conn: DbConn) -> JsonResult {
})))
}
#[get("/sync")]
fn sync_no_query(headers: Headers, conn: DbConn) -> JsonResult {
let sync_data = SyncData {
excludeDomains: false,
};
sync(sync_data, headers, conn)
}
#[get("/ciphers")]
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
@ -103,7 +136,7 @@ fn get_cipher_details(uuid: String, headers: Headers, conn: DbConn) -> JsonResul
#[allow(non_snake_case)]
pub struct CipherData {
// Id is optional as it is included only in bulk share
Id: Option<String>,
pub Id: Option<String>,
// Folder id is not included in import
FolderId: Option<String>,
// TODO: Some of these might appear all the time, no need for Option
@ -129,13 +162,25 @@ pub struct CipherData {
Favorite: Option<bool>,
PasswordHistory: Option<Value>,
// These are used during key rotation
#[serde(rename = "Attachments")]
_Attachments: Option<Value>, // Unused, contains map of {id: filename}
Attachments2: Option<HashMap<String, Attachments2Data>>
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct Attachments2Data {
FileName: String,
Key: String,
}
#[post("/ciphers/admin", data = "<data>")]
fn post_ciphers_admin(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
let data: ShareCipherData = data.into_inner().data;
let mut cipher = Cipher::new(data.Cipher.Type.clone(), data.Cipher.Name.clone());
let mut cipher = Cipher::new(data.Cipher.Type, data.Cipher.Name.clone());
cipher.user_uuid = Some(headers.user.uuid.clone());
match cipher.save(&conn) {
Ok(()) => (),
@ -188,6 +233,28 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
}
}
// Modify attachments name and keys when rotating
if let Some(attachments) = data.Attachments2 {
for (id, attachment) in attachments {
let mut saved_att = match Attachment::find_by_id(&id, &conn) {
Some(att) => att,
None => err!("Attachment doesn't exist")
};
if saved_att.cipher_uuid != cipher.uuid {
err!("Attachment is not owned by the cipher")
}
saved_att.key = Some(attachment.Key);
saved_att.file_name = attachment.FileName;
match saved_att.save(&conn) {
Ok(()) => (),
Err(_) => err!("Failed to save attachment")
};
}
}
let type_data_opt = match data.Type {
1 => data.Login,
2 => data.SecureNote,
@ -219,7 +286,7 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
match cipher.save(&conn) {
Ok(()) => (),
Err(_) => println!("Error: Failed to save cipher")
Err(_) => err!("Failed to save cipher")
};
ws.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn));
@ -266,7 +333,6 @@ fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbC
}
// Read the relations between folders and ciphers
use std::collections::HashMap;
let mut relations_map = HashMap::new();
for relation in data.FolderRelationships {
@ -509,37 +575,52 @@ fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers
let base_path = Path::new(&CONFIG.attachments_folder).join(&cipher.uuid);
let mut attachment_key = None;
Multipart::with_body(data.open(), boundary).foreach_entry(|mut field| {
// This is provided by the client, don't trust it
let name = field.headers.filename.expect("No filename provided");
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
let path = base_path.join(&file_name);
let size = match field.data.save()
.memory_threshold(0)
.size_limit(None)
.with_path(path) {
SaveResult::Full(SavedData::File(_, size)) => size as i32,
SaveResult::Full(other) => {
println!("Attachment is not a file: {:?}", other);
return;
match field.headers.name.as_str() {
"key" => {
use std::io::Read;
let mut key_buffer = String::new();
if field.data.read_to_string(&mut key_buffer).is_ok() {
attachment_key = Some(key_buffer);
}
},
SaveResult::Partial(_, reason) => {
println!("Partial result: {:?}", reason);
return;
},
SaveResult::Error(e) => {
println!("Error: {:?}", e);
return;
}
};
"data" => {
// This is provided by the client, don't trust it
let name = field.headers.filename.expect("No filename provided");
let attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
match attachment.save(&conn) {
Ok(()) => (),
Err(_) => println!("Error: failed to save attachment")
};
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
let path = base_path.join(&file_name);
let size = match field.data.save()
.memory_threshold(0)
.size_limit(None)
.with_path(path) {
SaveResult::Full(SavedData::File(_, size)) => size as i32,
SaveResult::Full(other) => {
error!("Attachment is not a file: {:?}", other);
return;
},
SaveResult::Partial(_, reason) => {
error!("Partial result: {:?}", reason);
return;
},
SaveResult::Error(e) => {
error!("Error: {:?}", e);
return;
}
};
let mut attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
attachment.key = attachment_key.clone();
match attachment.save(&conn) {
Ok(()) => (),
Err(_) => error!("Failed to save attachment")
};
},
_ => error!("Invalid multipart name")
}
}).expect("Error processing multipart data");
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
@ -670,7 +751,7 @@ fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn,
}
match cipher.save(&conn) {
Ok(()) => (),
Err(_) => println!("Error: Failed to save cipher")
Err(_) => err!("Failed to save cipher")
};
ws.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&conn));
}
@ -708,8 +789,7 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, ws
for f in Folder::find_by_user(&user.uuid, &conn) {
if f.delete(&conn).is_err() {
err!("Failed deleting folder")
}
else {
} else {
ws.send_folder_update(UpdateType::SyncFolderCreate, &f);
}
}
@ -761,6 +841,6 @@ fn _delete_cipher_attachment_by_id(uuid: &str, attachment_id: &str, headers: &He
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
Ok(())
}
Err(_) => err!("Deleting attachement failed")
Err(_) => err!("Deleting attachment failed")
}
}

View file

@ -1,11 +1,26 @@
use rocket::State;
use rocket_contrib::{Json, Value};
use rocket_contrib::json::Json;
use serde_json::Value;
use db::DbConn;
use db::models::*;
use crate::db::DbConn;
use crate::db::models::*;
use api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
use auth::Headers;
use crate::api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
use crate::auth::Headers;
use rocket::Route;
pub fn routes() -> Vec<Route> {
routes![
get_folders,
get_folder,
post_folders,
post_folder,
put_folder,
delete_folder_post,
delete_folder,
]
}
#[get("/folders")]
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {

View file

@ -4,182 +4,69 @@ mod folders;
mod organizations;
pub(crate) mod two_factor;
use self::accounts::*;
use self::ciphers::*;
use self::folders::*;
use self::organizations::*;
use self::two_factor::*;
pub fn routes() -> Vec<Route> {
routes![
register,
profile,
put_profile,
post_profile,
get_public_keys,
post_keys,
post_password,
post_kdf,
post_sstamp,
post_email_token,
post_email,
delete_account,
post_delete_account,
revision_date,
password_hint,
prelogin,
sync,
sync_no_query,
get_ciphers,
get_cipher,
get_cipher_admin,
get_cipher_details,
post_ciphers,
put_cipher_admin,
post_ciphers_admin,
post_ciphers_create,
post_ciphers_import,
post_attachment,
post_attachment_admin,
post_attachment_share,
delete_attachment_post,
delete_attachment_post_admin,
delete_attachment,
delete_attachment_admin,
post_cipher_admin,
post_cipher_share,
put_cipher_share,
put_cipher_share_seleted,
post_cipher,
put_cipher,
delete_cipher_post,
delete_cipher_post_admin,
delete_cipher,
delete_cipher_admin,
delete_cipher_selected,
delete_cipher_selected_post,
delete_all,
move_cipher_selected,
move_cipher_selected_put,
get_folders,
get_folder,
post_folders,
post_folder,
put_folder,
delete_folder_post,
delete_folder,
get_twofactor,
get_recover,
recover,
disable_twofactor,
disable_twofactor_put,
generate_authenticator,
activate_authenticator,
activate_authenticator_put,
generate_u2f,
generate_u2f_challenge,
activate_u2f,
activate_u2f_put,
generate_yubikey,
activate_yubikey,
activate_yubikey_put,
get_organization,
create_organization,
delete_organization,
post_delete_organization,
leave_organization,
get_user_collections,
get_org_collections,
get_org_collection_detail,
get_collection_users,
put_organization,
post_organization,
post_organization_collections,
delete_organization_collection_user,
post_organization_collection_delete_user,
post_organization_collection_update,
put_organization_collection_update,
delete_organization_collection,
post_organization_collection_delete,
post_collections_update,
post_collections_admin,
put_collections_admin,
get_org_details,
get_org_users,
send_invite,
confirm_invite,
get_user,
edit_user,
put_organization_user,
delete_user,
post_delete_user,
post_reinvite_user,
post_org_import,
let mut mod_routes = routes![
clear_device_token,
put_device_token,
get_eq_domains,
post_eq_domains,
put_eq_domains,
];
]
let mut routes = Vec::new();
routes.append(&mut accounts::routes());
routes.append(&mut ciphers::routes());
routes.append(&mut folders::routes());
routes.append(&mut organizations::routes());
routes.append(&mut two_factor::routes());
routes.append(&mut mod_routes);
routes
}
///
/// Move this somewhere else
///
use rocket::Route;
use rocket_contrib::{Json, Value};
use rocket_contrib::json::Json;
use serde_json::Value;
use db::DbConn;
use db::models::*;
use crate::db::DbConn;
use api::{JsonResult, EmptyResult, JsonUpcase};
use auth::Headers;
use crate::api::{EmptyResult, JsonResult, JsonUpcase};
use crate::auth::Headers;
#[put("/devices/identifier/<uuid>/clear-token", data = "<data>")]
fn clear_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
let _data: Value = data.into_inner();
let device = match Device::find_by_uuid(&uuid, &conn) {
Some(device) => device,
None => err!("Device not found")
};
#[put("/devices/identifier/<uuid>/clear-token")]
fn clear_device_token(uuid: String) -> EmptyResult {
// This endpoint doesn't have auth header
if device.user_uuid != headers.user.uuid {
err!("Device not owned by user")
}
let _ = uuid;
// uuid is not related to deviceId
match device.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed deleting device")
}
// This only clears push token
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
Ok(())
}
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
fn put_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> JsonResult {
let _data: Value = data.into_inner();
let device = match Device::find_by_uuid(&uuid, &conn) {
Some(device) => device,
None => err!("Device not found")
};
fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) -> JsonResult {
let _data: Value = data.into_inner().data;
// Data has a single string value "PushToken"
let _ = uuid;
// uuid is not related to deviceId
if device.user_uuid != headers.user.uuid {
err!("Device not owned by user")
}
// TODO: This should save the push token, but we don't have push functionality
// TODO: What does this do?
err!("Not implemented")
Ok(Json(json!({
"Id": headers.device.uuid,
"Name": headers.device.name,
"Type": headers.device.type_,
"Identifier": headers.device.uuid,
"CreationDate": crate::util::format_date(&headers.device.created_at),
})))
}
#[derive(Serialize, Deserialize, Debug)]
@ -213,7 +100,6 @@ fn get_eq_domains(headers: Headers) -> JsonResult {
})))
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct EquivDomainData {
@ -236,9 +122,8 @@ fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: Db
match user.save(&conn) {
Ok(()) => Ok(Json(json!({}))),
Err(_) => err!("Failed to save user")
Err(_) => err!("Failed to save user"),
}
}
#[put("/settings/domains", data = "<data>")]

View file

@ -1,14 +1,53 @@
use rocket::State;
use rocket_contrib::{Json, Value};
use CONFIG;
use db::DbConn;
use db::models::*;
use rocket::request::Form;
use rocket_contrib::json::Json;
use serde_json::Value;
use api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase, WebSocketUsers, UpdateType};
use auth::{Headers, AdminHeaders, OwnerHeaders};
use crate::CONFIG;
use crate::db::DbConn;
use crate::db::models::*;
use crate::api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase, WebSocketUsers, UpdateType};
use crate::auth::{Headers, AdminHeaders, OwnerHeaders};
use serde::{Deserialize, Deserializer};
use rocket::Route;
pub fn routes() -> Vec<Route> {
routes![
get_organization,
create_organization,
delete_organization,
post_delete_organization,
leave_organization,
get_user_collections,
get_org_collections,
get_org_collection_detail,
get_collection_users,
put_organization,
post_organization,
post_organization_collections,
delete_organization_collection_user,
post_organization_collection_delete_user,
post_organization_collection_update,
put_organization_collection_update,
delete_organization_collection,
post_organization_collection_delete,
get_org_details,
get_org_users,
send_invite,
confirm_invite,
get_user,
edit_user,
put_organization_user,
delete_user,
post_delete_user,
post_reinvite_user,
post_org_import,
]
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
@ -315,14 +354,14 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders,
}
#[derive(FromForm)]
#[allow(non_snake_case)]
struct OrgIdData {
organizationId: String
#[form(field = "organizationId")]
organization_id: String
}
#[get("/ciphers/organization-details?<data>")]
fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_org(&data.organizationId, &conn);
#[get("/ciphers/organization-details?<data..>")]
fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_org(&data.organization_id, &conn);
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
Ok(Json(json!({
@ -643,10 +682,10 @@ struct RelationsData {
Value: usize,
}
#[post("/ciphers/import-organization?<query>", data = "<data>")]
fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
#[post("/ciphers/import-organization?<query..>", data = "<data>")]
fn post_org_import(query: Form<OrgIdData>, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
let data: ImportData = data.into_inner().data;
let org_id = query.organizationId;
let org_id = query.into_inner().organization_id;
let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
Some(user) => user,
@ -700,4 +739,4 @@ fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Head
Ok(()) => Ok(()),
Err(_) => err!("Failed to update the revision, please log out and log back in to finish import.")
}
}
}

View file

@ -1,16 +1,40 @@
use data_encoding::BASE32;
use rocket_contrib::{Json, Value};
use rocket_contrib::json::Json;
use serde_json;
use serde_json::Value;
use db::{
use crate::db::{
models::{TwoFactor, TwoFactorType, User},
DbConn,
};
use crypto;
use crate::crypto;
use api::{ApiResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
use auth::Headers;
use crate::api::{ApiResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
use crate::auth::Headers;
use rocket::Route;
pub fn routes() -> Vec<Route> {
routes![
get_twofactor,
get_recover,
recover,
disable_twofactor,
disable_twofactor_put,
generate_authenticator,
activate_authenticator,
activate_authenticator_put,
generate_u2f,
generate_u2f_challenge,
activate_u2f,
activate_u2f_put,
generate_yubikey,
activate_yubikey,
activate_yubikey_put,
]
}
#[get("/two-factor")]
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
@ -50,7 +74,7 @@ struct RecoverTwoFactor {
fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
let data: RecoverTwoFactor = data.into_inner().data;
use db::models::User;
use crate::db::models::User;
// Get the user
let mut user = match User::find_by_mail(&data.Email, &conn) {
@ -219,7 +243,7 @@ fn _generate_recover_code(user: &mut User, conn: &DbConn) {
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
user.totp_recover = Some(totp_recover);
if user.save(conn).is_err() {
println!("Error: Failed to save the user's two factor recovery code")
error!("Failed to save the user's two factor recovery code")
}
}
}
@ -228,7 +252,7 @@ use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
use u2f::protocol::{Challenge, U2f};
use u2f::register::Registration;
use CONFIG;
use crate::CONFIG;
const U2F_VERSION: &str = "U2F_V2";
@ -376,7 +400,7 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
})))
}
Err(e) => {
println!("Error: {:#?}", e);
error!("{:#?}", e);
err!("Error activating u2f")
}
}
@ -480,11 +504,11 @@ pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> Api
match response {
Ok(new_counter) => {
_counter = new_counter;
println!("O {:#}", new_counter);
info!("O {:#}", new_counter);
return Ok(());
}
Err(e) => {
println!("E {:#}", e);
info!("E {:#}", e);
break;
}
}
@ -544,9 +568,8 @@ fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
let mut result = json!({});
for i in 0..yubikeys.len() {
let ref key = &yubikeys[i];
result[format!("Key{}", i+1)] = Value::String(key.to_string());
for (i, key) in yubikeys.into_iter().enumerate() {
result[format!("Key{}", i+1)] = Value::String(key);
}
result
@ -630,7 +653,7 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
let yubikeys = parse_yubikeys(&data);
if yubikeys.len() == 0 {
if yubikeys.is_empty() {
return Ok(Json(json!({
"Enabled": false,
"Object": "twoFactorU2f",

View file

@ -7,7 +7,7 @@ use rocket::http::ContentType;
use reqwest;
use CONFIG;
use crate::CONFIG;
pub fn routes() -> Vec<Route> {
routes![icon]
@ -43,7 +43,7 @@ fn get_icon (domain: &str) -> Vec<u8> {
icon
},
Err(e) => {
println!("Error downloading icon: {:?}", e);
error!("Error downloading icon: {:?}", e);
get_fallback_icon()
}
}
@ -71,7 +71,7 @@ fn get_icon_url(domain: &str) -> String {
}
fn download_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
println!("Downloading icon for {}...", url);
info!("Downloading icon for {}...", url);
let mut res = reqwest::get(url)?;
res = res.error_for_status()?;
@ -105,7 +105,7 @@ fn get_fallback_icon() -> Vec<u8> {
icon
},
Err(e) => {
println!("Error downloading fallback icon: {:?}", e);
error!("Error downloading fallback icon: {:?}", e);
vec![]
}
}

View file

@ -1,42 +1,43 @@
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use rocket::request::LenientForm;
use rocket::Route;
use rocket::request::{self, Form, FormItems, FromForm, FromRequest, Request};
use rocket::{Outcome, Route};
use rocket_contrib::{Json, Value};
use rocket_contrib::json::Json;
use serde_json::Value;
use num_traits::FromPrimitive;
use db::models::*;
use db::DbConn;
use crate::db::models::*;
use crate::db::DbConn;
use util::{self, JsonMap};
use crate::util::{self, JsonMap};
use api::{ApiResult, JsonResult};
use crate::api::{ApiResult, EmptyResult, JsonResult};
use CONFIG;
use crate::auth::ClientIp;
use crate::CONFIG;
pub fn routes() -> Vec<Route> {
routes![login]
}
#[post("/connect/token", data = "<connect_data>")]
fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn, socket: Option<SocketAddr>) -> JsonResult {
let data = connect_data.get();
#[post("/connect/token", data = "<data>")]
fn login(data: LenientForm<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult {
let data: ConnectData = data.into_inner();
validate_data(&data)?;
match data.grant_type {
GrantType::RefreshToken => _refresh_login(data, device_type, conn),
GrantType::Password => _password_login(data, device_type, conn, socket),
GrantType::refresh_token => _refresh_login(data, conn),
GrantType::password => _password_login(data, conn, ip),
}
}
fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) -> JsonResult {
fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
// Extract token
let token = data.get("refresh_token");
let token = data.refresh_token.unwrap();
// Get device by refresh token
let mut device = match Device::find_by_refresh_token(token, &conn) {
let mut device = match Device::find_by_refresh_token(&token, &conn) {
Some(device) => device,
None => err!("Invalid refresh token"),
};
@ -47,90 +48,71 @@ fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) ->
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
match device.save(&conn) {
Ok(()) => Ok(Json(json!({
"access_token": access_token,
"expires_in": expires_in,
"token_type": "Bearer",
"refresh_token": device.refresh_token,
"Key": user.key,
"PrivateKey": user.private_key,
}))),
Err(_) => err!("Failed to add device to user")
Ok(()) => Ok(Json(json!({
"access_token": access_token,
"expires_in": expires_in,
"token_type": "Bearer",
"refresh_token": device.refresh_token,
"Key": user.key,
"PrivateKey": user.private_key,
}))),
Err(e) => err!("Failed to add device to user", e),
}
}
fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn, remote: Option<SocketAddr>) -> JsonResult {
// Get the ip for error reporting
let ip = match remote {
Some(ip) => ip.ip(),
None => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
};
fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult {
// Validate scope
let scope = data.get("scope");
let scope = data.scope.as_ref().unwrap();
if scope != "api offline_access" {
err!("Scope not supported")
}
// Get the user
let username = data.get("username");
let username = data.username.as_ref().unwrap();
let user = match User::find_by_mail(username, &conn) {
Some(user) => user,
None => err!(format!(
"Username or password is incorrect. Try again. IP: {}. Username: {}.",
ip, username
ip.ip, username
)),
};
// Check password
let password = data.get("password");
let password = data.password.as_ref().unwrap();
if !user.check_valid_password(password) {
err!(format!(
"Username or password is incorrect. Try again. IP: {}. Username: {}.",
ip, username
ip.ip, username
))
}
// Let's only use the header and ignore the 'devicetype' parameter
let device_type_num = device_type.0;
let (device_id, device_name) = if data.is_device {
(
data.get("deviceidentifier").clone(),
data.get("devicename").clone(),
)
} else {
(format!("web-{}", user.uuid), String::from("web"))
};
let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(0);
let device_id = data.device_identifier.clone().unwrap_or_else(crate::util::get_uuid);
let device_name = data.device_name.clone().unwrap_or("unknown_device".into());
// Find device or create new
let mut device = match Device::find_by_uuid(&device_id, &conn) {
Some(device) => {
// Check if valid device
// Check if owned device, and recreate if not
if device.user_uuid != user.uuid {
match device.delete(&conn) {
Ok(()) => Device::new(device_id, user.uuid.clone(), device_name, device_type_num),
Err(_) => err!("Tried to delete device not owned by user, but failed")
}
info!("Device exists but is owned by another user. The old device will be discarded");
Device::new(device_id, user.uuid.clone(), device_name, device_type)
} else {
device
}
}
None => {
// Create new device
Device::new(device_id, user.uuid.clone(), device_name, device_type_num)
}
None => Device::new(device_id, user.uuid.clone(), device_name, device_type)
};
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?;
let twofactor_token = twofactor_auth(&user.uuid, &data.clone(), &mut device, &conn)?;
// Common
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
if device.save(&conn).is_err() {
err!("Failed to add device to user")
if let Err(e) = device.save(&conn) {
err!("Failed to add device to user", e)
}
let mut result = json!({
@ -147,6 +129,7 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn, re
result["TwoFactorToken"] = Value::String(token);
}
info!("User {} logged in successfully. IP: {}", username, ip.ip);
Ok(Json(result))
}
@ -167,13 +150,10 @@ fn twofactor_auth(
return Ok(None);
}
let provider = match util::try_parse_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 provider = data.two_factor_provider.unwrap_or(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,
let twofactor_code = match data.two_factor_token {
Some(ref code) => code,
None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
};
@ -181,8 +161,8 @@ fn twofactor_auth(
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
match device.twofactor_remember {
Some(ref remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here
_ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
}
}
@ -204,13 +184,13 @@ fn twofactor_auth(
}
Some(TwoFactorType::U2f) => {
use api::core::two_factor;
use crate::api::core::two_factor;
two_factor::validate_u2f_login(user_uuid, twofactor_code, conn)?;
two_factor::validate_u2f_login(user_uuid, &twofactor_code, conn)?;
}
Some(TwoFactorType::YubiKey) => {
use api::core::two_factor;
use crate::api::core::two_factor;
two_factor::validate_yubikey_login(user_uuid, twofactor_code, conn)?;
}
@ -218,7 +198,7 @@ fn twofactor_auth(
_ => err!("Invalid two factor provider"),
}
if util::try_parse_string_or(data.get_opt("twoFactorRemember"), 0) == 1 {
if data.two_factor_remember.unwrap_or(0) == 1 {
Ok(Some(device.refresh_twofactor_remember()))
} else {
device.delete_twofactor_remember();
@ -227,7 +207,7 @@ fn twofactor_auth(
}
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
use api::core::two_factor;
use crate::api::core::two_factor;
let mut result = json!({
"error" : "invalid_grant",
@ -289,93 +269,60 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
Ok(result)
}
#[derive(Clone, Copy)]
struct DeviceType(i32);
impl<'a, 'r> FromRequest<'a, 'r> for DeviceType {
type Error = &'static str;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let headers = request.headers();
let type_opt = headers.get_one("Device-Type");
let type_num = util::try_parse_string_or(type_opt, 0);
Outcome::Success(DeviceType(type_num))
}
}
#[derive(Debug)]
#[derive(FromForm, Debug, Clone)]
#[allow(non_snake_case)]
struct ConnectData {
grant_type: GrantType,
is_device: bool,
data: HashMap<String, String>,
// Needed for grant_type="refresh_token"
refresh_token: Option<String>,
// Needed for grant_type="password"
client_id: Option<String>, // web, cli, desktop, browser, mobile
password: Option<String>,
scope: Option<String>,
username: Option<String>,
#[form(field = "deviceIdentifier")]
device_identifier: Option<String>,
#[form(field = "deviceName")]
device_name: Option<String>,
#[form(field = "deviceType")]
device_type: Option<String>,
// Needed for two-factor auth
#[form(field = "twoFactorProvider")]
two_factor_provider: Option<i32>,
#[form(field = "twoFactorToken")]
two_factor_token: Option<String>,
#[form(field = "twoFactorRemember")]
two_factor_remember: Option<i32>,
}
#[derive(Debug, Copy, Clone)]
#[derive(FromFormValue, Debug, Clone, Copy)]
#[allow(non_camel_case_types)]
enum GrantType {
RefreshToken,
Password,
refresh_token,
password,
}
impl ConnectData {
fn get(&self, key: &str) -> &String {
&self.data[&key.to_lowercase()]
}
fn get_opt(&self, key: &str) -> Option<&String> {
self.data.get(&key.to_lowercase())
}
}
const VALUES_REFRESH: [&str; 1] = ["refresh_token"];
const VALUES_PASSWORD: [&str; 5] = ["client_id", "grant_type", "password", "scope", "username"];
const VALUES_DEVICE: [&str; 3] = ["deviceidentifier", "devicename", "devicetype"];
impl<'f> FromForm<'f> for ConnectData {
type Error = String;
fn from_form(items: &mut FormItems<'f>, _strict: bool) -> Result<Self, Self::Error> {
let mut data = HashMap::new();
// Insert data into map
for (key, value) in items {
match (key.url_decode(), value.url_decode()) {
(Ok(key), Ok(value)) => data.insert(key.to_lowercase(), value),
_ => return Err("Error decoding key or value".to_string()),
};
fn validate_data(data: &ConnectData) -> EmptyResult {
match data.grant_type {
GrantType::refresh_token => {
_check_is_some(&data.refresh_token, "refresh_token cannot be blank")
}
// 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 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,
})
}
}
fn check_values(map: &HashMap<String, String>, values: &[&str]) -> Result<bool, String> {
for value in values {
if !map.contains_key(*value) {
return Err(format!("{} cannot be blank", value));
GrantType::password => {
_check_is_some(&data.client_id, "client_id cannot be blank")?;
_check_is_some(&data.password, "password cannot be blank")?;
_check_is_some(&data.scope, "scope cannot be blank")?;
_check_is_some(&data.username, "username cannot be blank")
}
}
Ok(true)
}
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
if value.is_none() {
err!(msg)
}
Ok(())
}

View file

@ -12,14 +12,15 @@ pub use self::notifications::routes as notifications_routes;
pub use self::notifications::{start_notification_server, WebSocketUsers, UpdateType};
use rocket::response::status::BadRequest;
use rocket_contrib::Json;
use rocket_contrib::json::Json;
use serde_json::Value;
// Type aliases for API methods results
type ApiResult<T> = Result<T, BadRequest<Json>>;
type JsonResult = ApiResult<Json>;
type ApiResult<T> = Result<T, BadRequest<Json<Value>>>;
type JsonResult = ApiResult<Json<Value>>;
type EmptyResult = ApiResult<()>;
use util;
use crate::util;
type JsonUpcase<T> = Json<util::UpCase<T>>;
// Common structs representing JSON data received

View file

@ -1,12 +1,12 @@
use rocket::Route;
use rocket_contrib::Json;
use rocket_contrib::json::Json;
use serde_json::Value as JsonValue;
use api::JsonResult;
use auth::Headers;
use db::DbConn;
use crate::api::JsonResult;
use crate::auth::Headers;
use crate::db::DbConn;
use CONFIG;
use crate::CONFIG;
pub fn routes() -> Vec<Route> {
routes![negotiate, websockets_err]
@ -19,7 +19,7 @@ fn websockets_err() -> JsonResult {
#[post("/hub/negotiate")]
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
use crypto;
use crate::crypto;
use data_encoding::BASE64URL;
let conn_id = BASE64URL.encode(&crypto::get_random(vec![0u8; 16]));
@ -52,7 +52,7 @@ use chashmap::CHashMap;
use chrono::NaiveDateTime;
use serde_json::from_str;
use db::models::{Cipher, Folder, User};
use crate::db::models::{Cipher, Folder, User};
use rmpv::Value;
@ -139,7 +139,7 @@ impl Handler for WSHandler {
let _id = &query_split[1][3..];
// Validate the user
use auth;
use crate::auth;
let claims = match auth::decode_jwt(access_token) {
Ok(claims) => claims,
Err(_) => {
@ -169,7 +169,7 @@ impl Handler for WSHandler {
}
fn on_message(&mut self, msg: Message) -> ws::Result<()> {
println!("Server got message '{}'. ", msg);
info!("Server got message '{}'. ", msg);
if let Message::Text(text) = msg.clone() {
let json = &text[..text.len() - 1]; // Remove last char
@ -242,10 +242,10 @@ pub struct WebSocketUsers {
}
impl WebSocketUsers {
fn send_update(&self, user_uuid: &String, data: Vec<u8>) -> ws::Result<()> {
fn send_update(&self, user_uuid: &String, data: &[u8]) -> ws::Result<()> {
if let Some(user) = self.map.get(user_uuid) {
for sender in user.iter() {
sender.send(data.clone())?;
sender.send(data)?;
}
}
Ok(())
@ -262,7 +262,7 @@ impl WebSocketUsers {
ut,
);
self.send_update(&user.uuid.clone(), data).ok();
self.send_update(&user.uuid.clone(), &data).ok();
}
pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) {
@ -275,10 +275,10 @@ impl WebSocketUsers {
ut,
);
self.send_update(&folder.user_uuid, data).ok();
self.send_update(&folder.user_uuid, &data).ok();
}
pub fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &Vec<String>) {
pub fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &[String]) {
let user_uuid = convert_option(cipher.user_uuid.clone());
let org_uuid = convert_option(cipher.organization_uuid.clone());
@ -294,7 +294,7 @@ impl WebSocketUsers {
);
for uuid in user_uuids {
self.send_update(&uuid, data.clone()).ok();
self.send_update(&uuid, &data).ok();
}
}
}

View file

@ -6,9 +6,10 @@ use rocket::response::{self, NamedFile, Responder};
use rocket::response::content::Content;
use rocket::http::{ContentType, Status};
use rocket::Route;
use rocket_contrib::{Json, Value};
use rocket_contrib::json::Json;
use serde_json::Value;
use CONFIG;
use crate::CONFIG;
pub fn routes() -> Vec<Route> {
if CONFIG.web_vault_enabled {
@ -73,7 +74,7 @@ fn attachments(uuid: String, file: PathBuf) -> io::Result<NamedFile> {
#[get("/alive")]
fn alive() -> Json<String> {
use util::format_date;
use crate::util::format_date;
use chrono::Utc;
Json(format_date(&Utc::now().naive_utc()))

View file

@ -1,22 +1,21 @@
///
/// JWT Handling
///
use util::read_file;
use crate::util::read_file;
use chrono::Duration;
use jwt;
use jsonwebtoken::{self, Algorithm, Header};
use serde::ser::Serialize;
use CONFIG;
use crate::CONFIG;
const JWT_ALGORITHM: jwt::Algorithm = jwt::Algorithm::RS256;
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
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 JWT_HEADER: Header = Header::new(JWT_ALGORITHM);
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key) {
Ok(key) => key,
@ -30,17 +29,17 @@ lazy_static! {
}
pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
match jwt::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
match jsonwebtoken::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
Ok(token) => token,
Err(e) => panic!("Error encoding jwt {}", e)
}
}
pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
let validation = jwt::Validation {
let validation = jsonwebtoken::Validation {
leeway: 30, // 30 seconds
validate_exp: true,
validate_iat: true,
validate_iat: false, // IssuedAt is the same as NotBefore
validate_nbf: true,
aud: None,
iss: Some(JWT_ISSUER.clone()),
@ -48,10 +47,10 @@ pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
algorithms: vec![JWT_ALGORITHM],
};
match jwt::decode(token, &PUBLIC_RSA_KEY, &validation) {
match jsonwebtoken::decode(token, &PUBLIC_RSA_KEY, &validation) {
Ok(decoded) => Ok(decoded.claims),
Err(msg) => {
println!("Error validating jwt - {:#?}", msg);
error!("Error validating jwt - {:#?}", msg);
Err(msg.to_string())
}
}
@ -76,6 +75,7 @@ pub struct JWTClaims {
pub orgowner: Vec<String>,
pub orgadmin: Vec<String>,
pub orguser: Vec<String>,
pub orgmanager: Vec<String>,
// user security_stamp
pub sstamp: String,
@ -90,12 +90,11 @@ pub struct JWTClaims {
///
/// Bearer token authentication
///
use rocket::Outcome;
use rocket::request::{self, Request, FromRequest};
use db::DbConn;
use db::models::{User, Organization, UserOrganization, UserOrgType, UserOrgStatus, Device};
use crate::db::DbConn;
use crate::db::models::{User, Organization, UserOrganization, UserOrgType, UserOrgStatus, Device};
pub struct Headers {
pub host: String,
@ -139,13 +138,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
// Get access_token
let access_token: &str = match request.headers().get_one("Authorization") {
Some(a) => {
match a.rsplit("Bearer ").next() {
Some(split) => split,
None => err_handler!("No access token provided")
}
}
None => err_handler!("No access token provided")
Some(a) => match a.rsplit("Bearer ").next() {
Some(split) => split,
None => err_handler!("No access token provided"),
},
None => err_handler!("No access token provided"),
};
// Check JWT token is valid and get device and user from it
@ -192,13 +189,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match request.guard::<Headers>() {
Outcome::Forward(f) => Outcome::Forward(f),
Outcome::Forward(_) => Outcome::Forward(()),
Outcome::Failure(f) => Outcome::Failure(f),
Outcome::Success(headers) => {
// org_id is expected to be the first dynamic param
match request.get_param::<String>(0) {
Err(_) => err_handler!("Error getting the organization id"),
Ok(org_id) => {
// org_id is expected to be the second param ("/organizations/<org_id>")
match request.get_param::<String>(1) {
Some(Ok(org_id)) => {
let conn = match request.guard::<DbConn>() {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB")
@ -226,14 +222,15 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
device: headers.device,
user: headers.user,
org_user_type: {
if let Some(org_usr_type) = UserOrgType::from_i32(&org_user.type_) {
if let Some(org_usr_type) = UserOrgType::from_i32(org_user.type_) {
org_usr_type
} else { // This should only happen if the DB is corrupted
err_handler!("Unknown user type in the database")
}
},
})
}
},
_ => err_handler!("Error getting the organization id"),
}
}
}
@ -252,11 +249,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match request.guard::<OrgHeaders>() {
Outcome::Forward(f) => Outcome::Forward(f),
Outcome::Forward(_) => Outcome::Forward(()),
Outcome::Failure(f) => Outcome::Failure(f),
Outcome::Success(headers) => {
if headers.org_user_type >= UserOrgType::Admin {
Outcome::Success(Self{
Outcome::Success(Self {
host: headers.host,
device: headers.device,
user: headers.user,
@ -281,11 +278,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for OwnerHeaders {
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match request.guard::<OrgHeaders>() {
Outcome::Forward(f) => Outcome::Forward(f),
Outcome::Forward(_) => Outcome::Forward(()),
Outcome::Failure(f) => Outcome::Failure(f),
Outcome::Success(headers) => {
if headers.org_user_type == UserOrgType::Owner {
Outcome::Success(Self{
Outcome::Success(Self {
host: headers.host,
device: headers.device,
user: headers.user,
@ -296,4 +293,26 @@ impl<'a, 'r> FromRequest<'a, 'r> for OwnerHeaders {
}
}
}
}
}
///
/// Client IP address detection
///
use std::net::IpAddr;
pub struct ClientIp {
pub ip: IpAddr,
}
impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let ip = match request.client_ip() {
Some(addr) => addr,
None => "0.0.0.0".parse().unwrap(),
};
Outcome::Success(ClientIp { ip })
}
}

View file

@ -9,7 +9,7 @@ use rocket::http::Status;
use rocket::request::{self, FromRequest};
use rocket::{Outcome, Request, State};
use CONFIG;
use crate::CONFIG;
/// An alias to the database connection used
type Connection = SqliteConnection;

View file

@ -1,7 +1,7 @@
use serde_json::Value as JsonValue;
use serde_json::Value;
use super::Cipher;
use CONFIG;
use crate::CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "attachments"]
@ -12,6 +12,7 @@ pub struct Attachment {
pub cipher_uuid: String,
pub file_name: String,
pub file_size: i32,
pub key: Option<String>
}
/// Local methods
@ -22,6 +23,7 @@ impl Attachment {
cipher_uuid,
file_name,
file_size,
key: None
}
}
@ -29,8 +31,8 @@ impl Attachment {
format!("{}/{}/{}", CONFIG.attachments_folder, self.cipher_uuid, self.id)
}
pub fn to_json(&self, host: &str) -> JsonValue {
use util::get_display_size;
pub fn to_json(&self, host: &str) -> Value {
use crate::util::get_display_size;
let web_path = format!("{}/attachments/{}/{}", host, self.cipher_uuid, self.id);
let display_size = get_display_size(self.file_size);
@ -41,6 +43,7 @@ impl Attachment {
"FileName": self.file_name,
"Size": self.file_size.to_string(),
"SizeName": display_size,
"Key": self.key,
"Object": "attachment"
})
}
@ -48,8 +51,8 @@ impl Attachment {
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::attachments;
use crate::db::DbConn;
use crate::db::schema::attachments;
/// Database methods
impl Attachment {
@ -61,39 +64,21 @@ impl Attachment {
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
use util;
use std::{thread, time};
crate::util::retry(
|| {
diesel::delete(attachments::table.filter(attachments::id.eq(&self.id)))
.execute(&**conn)
},
10,
)?;
let mut retries = 10;
loop {
match diesel::delete(
attachments::table.filter(
attachments::id.eq(&self.id)
)
).execute(&**conn) {
Ok(_) => break,
Err(err) => {
if retries < 1 {
println!("ERROR: Failed with 10 retries");
return Err(err)
} else {
retries -= 1;
println!("Had to retry! Retries left: {}", retries);
thread::sleep(time::Duration::from_millis(500));
continue
}
}
}
}
util::delete_file(&self.get_file_path());
crate::util::delete_file(&self.get_file_path());
Ok(())
}
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
for attachement in Attachment::find_by_cipher(&cipher_uuid, &conn) {
attachement.delete(&conn)?;
for attachment in Attachment::find_by_cipher(&cipher_uuid, &conn) {
attachment.delete(&conn)?;
}
Ok(())
}

View file

@ -1,7 +1,5 @@
use chrono::{NaiveDateTime, Utc};
use serde_json::Value as JsonValue;
use uuid::Uuid;
use serde_json::Value;
use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrganization, UserOrgType, UserOrgStatus};
@ -41,7 +39,7 @@ impl Cipher {
let now = Utc::now().naive_utc();
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
created_at: now,
updated_at: now,
@ -63,28 +61,28 @@ impl Cipher {
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::*;
use crate::db::DbConn;
use crate::db::schema::*;
/// Database methods
impl Cipher {
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> JsonValue {
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value {
use serde_json;
use util::format_date;
use crate::util::format_date;
use super::Attachment;
let attachments = Attachment::find_by_cipher(&self.uuid, conn);
let attachments_json: Vec<JsonValue> = attachments.iter().map(|c| c.to_json(host)).collect();
let attachments_json: Vec<Value> = attachments.iter().map(|c| c.to_json(host)).collect();
let fields_json: JsonValue = if let Some(ref fields) = self.fields {
let fields_json: Value = if let Some(ref fields) = self.fields {
serde_json::from_str(fields).unwrap()
} else { JsonValue::Null };
} else { Value::Null };
let password_history_json: JsonValue = if let Some(ref password_history) = self.password_history {
let password_history_json: Value = if let Some(ref password_history) = self.password_history {
serde_json::from_str(password_history).unwrap()
} else { JsonValue::Null };
} else { Value::Null };
let mut data_json: JsonValue = serde_json::from_str(&self.data).unwrap();
let mut data_json: Value = serde_json::from_str(&self.data).unwrap();
// TODO: ******* Backwards compat start **********
// To remove backwards compatibility, just remove this entire section
@ -234,7 +232,7 @@ impl Cipher {
}
pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
match ciphers::table
ciphers::table
.filter(ciphers::uuid.eq(&self.uuid))
.left_join(users_organizations::table.on(
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
@ -255,14 +253,11 @@ impl Cipher {
)
))
.select(ciphers::all_columns)
.first::<Self>(&**conn).ok() {
Some(_) => true,
None => false
}
.first::<Self>(&**conn).ok().is_some()
}
pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
match ciphers::table
ciphers::table
.filter(ciphers::uuid.eq(&self.uuid))
.left_join(users_organizations::table.on(
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
@ -281,10 +276,7 @@ impl Cipher {
)
))
.select(ciphers::all_columns)
.first::<Self>(&**conn).ok() {
Some(_) => true,
None => false
}
.first::<Self>(&**conn).ok().is_some()
}
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {

View file

@ -1,6 +1,4 @@
use serde_json::Value as JsonValue;
use uuid::Uuid;
use serde_json::Value;
use super::{Organization, UserOrganization, UserOrgType, UserOrgStatus};
@ -18,14 +16,14 @@ pub struct Collection {
impl Collection {
pub fn new(org_uuid: String, name: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
org_uuid,
name,
}
}
pub fn to_json(&self) -> JsonValue {
pub fn to_json(&self) -> Value {
json!({
"Id": self.uuid,
"OrganizationId": self.org_uuid,
@ -37,8 +35,8 @@ impl Collection {
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::*;
use crate::db::DbConn;
use crate::db::schema::*;
/// Database methods
impl Collection {
@ -150,15 +148,12 @@ impl Collection {
if user_org.access_all {
true
} else {
match users_collections::table.inner_join(collections::table)
users_collections::table.inner_join(collections::table)
.filter(users_collections::collection_uuid.eq(&self.uuid))
.filter(users_collections::user_uuid.eq(&user_uuid))
.filter(users_collections::read_only.eq(false))
.select(collections::all_columns)
.first::<Self>(&**conn).ok() {
None => false, // Read only or no access to collection
Some(_) => true,
}
.first::<Self>(&**conn).ok().is_some() // Read only or no access to collection
}
}
}

View file

@ -45,7 +45,7 @@ impl Device {
pub fn refresh_twofactor_remember(&mut self) -> String {
use data_encoding::BASE64;
use crypto;
use crate::crypto;
let twofactor_remember = BASE64.encode(&crypto::get_random(vec![0u8; 180]));
self.twofactor_remember = Some(twofactor_remember.clone());
@ -62,7 +62,7 @@ impl Device {
// If there is no refresh token, we create one
if self.refresh_token.is_empty() {
use data_encoding::BASE64URL;
use crypto;
use crate::crypto;
self.refresh_token = BASE64URL.encode(&crypto::get_random_64());
}
@ -71,14 +71,14 @@ impl Device {
let time_now = Utc::now().naive_utc();
self.updated_at = time_now;
let orgowner: Vec<_> = orgs.iter().filter(|o| o.type_ == 0).map(|o| o.org_uuid.clone()).collect();
let orgadmin: Vec<_> = orgs.iter().filter(|o| o.type_ == 1).map(|o| o.org_uuid.clone()).collect();
let orguser: Vec<_> = orgs.iter().filter(|o| o.type_ == 2).map(|o| o.org_uuid.clone()).collect();
let orgmanager: Vec<_> = orgs.iter().filter(|o| o.type_ == 3).map(|o| o.org_uuid.clone()).collect();
// Create the JWT claims struct, to send to the client
use auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER};
use crate::auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER};
let claims = JWTClaims {
nbf: time_now.timestamp(),
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
@ -93,6 +93,7 @@ impl Device {
orgowner,
orgadmin,
orguser,
orgmanager,
sstamp: user.security_stamp.to_string(),
device: self.uuid.to_string(),
@ -100,23 +101,29 @@ impl Device {
amr: vec!["Application".into()],
};
(encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds())
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::devices;
use crate::db::DbConn;
use crate::db::schema::devices;
/// Database methods
impl Device {
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
self.updated_at = Utc::now().naive_utc();
diesel::replace_into(devices::table)
.values(&*self).execute(&**conn).and(Ok(()))
crate::util::retry(
|| {
diesel::replace_into(devices::table)
.values(&*self)
.execute(&**conn)
},
10,
)
.and(Ok(()))
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {

View file

@ -1,7 +1,5 @@
use chrono::{NaiveDateTime, Utc};
use serde_json::Value as JsonValue;
use uuid::Uuid;
use serde_json::Value;
use super::{User, Cipher};
@ -33,7 +31,7 @@ impl Folder {
let now = Utc::now().naive_utc();
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
created_at: now,
updated_at: now,
@ -42,8 +40,8 @@ impl Folder {
}
}
pub fn to_json(&self) -> JsonValue {
use util::format_date;
pub fn to_json(&self) -> Value {
use crate::util::format_date;
json!({
"Id": self.uuid,
@ -65,8 +63,8 @@ impl FolderCipher {
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::{folders, folders_ciphers};
use crate::db::DbConn;
use crate::db::schema::{folders, folders_ciphers};
/// Database methods
impl Folder {

View file

@ -1,7 +1,6 @@
use std::cmp::Ordering;
use serde_json::Value as JsonValue;
use serde_json::Value;
use uuid::Uuid;
use super::{User, CollectionUser, Invitation};
#[derive(Debug, Identifiable, Queryable, Insertable)]
@ -78,10 +77,10 @@ impl PartialEq<i32> for UserOrgType {
impl PartialOrd<i32> for UserOrgType {
fn partial_cmp(&self, other: &i32) -> Option<Ordering> {
if let Some(other) = Self::from_i32(other) {
if let Some(other) = Self::from_i32(*other) {
return Some(self.cmp(&other))
}
return None
None
}
fn gt(&self, other: &i32) -> bool {
@ -108,10 +107,10 @@ impl PartialEq<UserOrgType> for i32 {
impl PartialOrd<UserOrgType> for i32 {
fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
if let Some(self_type) = UserOrgType::from_i32(self) {
if let Some(self_type) = UserOrgType::from_i32(*self) {
return Some(self_type.cmp(other))
}
return None
None
}
fn lt(&self, other: &UserOrgType) -> bool {
@ -141,7 +140,7 @@ impl UserOrgType {
}
}
pub fn from_i32(i: &i32) -> Option<Self> {
pub fn from_i32(i: i32) -> Option<Self> {
match i {
0 => Some(UserOrgType::Owner),
1 => Some(UserOrgType::Admin),
@ -159,7 +158,7 @@ impl Organization {
pub fn new(name: String, billing_email: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
name,
billing_email,
@ -174,7 +173,7 @@ impl Organization {
}
}
pub fn to_json(&self) -> JsonValue {
pub fn to_json(&self) -> Value {
json!({
"Id": self.uuid,
"Name": self.name,
@ -206,7 +205,7 @@ impl Organization {
impl UserOrganization {
pub fn new(user_uuid: String, org_uuid: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
user_uuid,
org_uuid,
@ -236,8 +235,8 @@ impl UserOrganization {
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::{organizations, users_organizations, users_collections, ciphers_collections};
use crate::db::DbConn;
use crate::db::schema::{organizations, users_organizations, users_collections, ciphers_collections};
/// Database methods
impl Organization {
@ -285,7 +284,7 @@ impl Organization {
}
impl UserOrganization {
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
pub fn to_json(&self, conn: &DbConn) -> Value {
let org = Organization::find_by_uuid(&self.org_uuid, conn).unwrap();
json!({
@ -313,7 +312,7 @@ impl UserOrganization {
})
}
pub fn to_json_user_details(&self, conn: &DbConn) -> JsonValue {
pub fn to_json_user_details(&self, conn: &DbConn) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
json!({
@ -330,7 +329,7 @@ impl UserOrganization {
})
}
pub fn to_json_collection_user_details(&self, read_only: bool, conn: &DbConn) -> JsonValue {
pub fn to_json_collection_user_details(&self, read_only: bool, conn: &DbConn) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
json!({
@ -345,7 +344,7 @@ impl UserOrganization {
})
}
pub fn to_json_details(&self, conn: &DbConn) -> JsonValue {
pub fn to_json_details(&self, conn: &DbConn) -> Value {
let coll_uuids = if self.access_all {
vec![] // If we have complete access, no need to fill the array
} else {

View file

@ -1,6 +1,4 @@
use serde_json::Value as JsonValue;
use uuid::Uuid;
use serde_json::Value;
use super::User;
@ -36,7 +34,7 @@ pub enum TwoFactorType {
impl TwoFactor {
pub fn new(user_uuid: String, type_: TwoFactorType, data: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
user_uuid,
type_: type_ as i32,
enabled: true,
@ -59,7 +57,7 @@ impl TwoFactor {
generated == totp_code
}
pub fn to_json(&self) -> JsonValue {
pub fn to_json(&self) -> Value {
json!({
"Enabled": self.enabled,
"Key": "", // This key and value vary
@ -67,7 +65,7 @@ impl TwoFactor {
})
}
pub fn to_json_list(&self) -> JsonValue {
pub fn to_json_list(&self) -> Value {
json!({
"Enabled": self.enabled,
"Type": self.type_,
@ -78,8 +76,8 @@ impl TwoFactor {
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::twofactor;
use crate::db::DbConn;
use crate::db::schema::twofactor;
/// Database methods
impl TwoFactor {

View file

@ -1,10 +1,8 @@
use chrono::{NaiveDateTime, Utc};
use serde_json::Value as JsonValue;
use serde_json::Value;
use uuid::Uuid;
use crypto;
use CONFIG;
use crate::crypto;
use crate::CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable)]
@ -50,7 +48,7 @@ impl User {
let email = mail.to_lowercase();
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
created_at: now,
updated_at: now,
name: email.clone(),
@ -61,7 +59,7 @@ impl User {
salt: crypto::get_random_64(),
password_iterations: CONFIG.password_iterations,
security_stamp: Uuid::new_v4().to_string(),
security_stamp: crate::util::get_uuid(),
password_hint: None,
private_key: None,
@ -97,11 +95,10 @@ impl User {
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();
self.security_stamp = crate::util::get_uuid();
}
pub fn is_server_admin(&self) -> bool {
@ -114,20 +111,20 @@ impl User {
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::{users, invitations};
use crate::db::DbConn;
use crate::db::schema::{users, invitations};
use super::{Cipher, Folder, Device, UserOrganization, UserOrgType};
/// Database methods
impl User {
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
pub fn to_json(&self, conn: &DbConn) -> Value {
use super::{UserOrganization, UserOrgType, UserOrgStatus, TwoFactor};
let mut orgs = UserOrganization::find_by_user(&self.uuid, conn);
if self.is_server_admin() {
orgs.push(UserOrganization::new_virtual(self.uuid.clone(), UserOrgType::Owner, UserOrgStatus::Confirmed));
}
let orgs_json: Vec<JsonValue> = orgs.iter().map(|c| c.to_json(&conn)).collect();
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect();
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
json!({
@ -181,7 +178,7 @@ impl User {
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
if let Some(mut user) = User::find_by_uuid(&uuid, conn) {
if user.update_revision(conn).is_err(){
println!("Warning: Failed to update revision for {}", user.email);
warn!("Failed to update revision for {}", user.email);
};
};
}

View file

@ -4,6 +4,7 @@ table! {
cipher_uuid -> Text,
file_name -> Text,
file_size -> Integer,
key -> Nullable<Text>,
}
}

View file

@ -4,7 +4,7 @@ use lettre::smtp::ConnectionReuseParameters;
use lettre::smtp::authentication::Credentials;
use lettre_email::EmailBuilder;
use MailConfig;
use crate::MailConfig;
fn mailer(config: &MailConfig) -> SmtpTransport {
let client_security = if config.smtp_ssl {

View file

@ -1,41 +1,15 @@
#![feature(plugin, custom_derive, vec_remove_item, try_trait)]
#![plugin(rocket_codegen)]
#![recursion_limit="128"]
#![feature(proc_macro_hygiene, decl_macro, vec_remove_item, try_trait)]
#![recursion_limit = "128"]
#![allow(proc_macro_derive_resolution_fallback)] // TODO: Remove this when diesel update fixes warnings
extern crate rocket;
extern crate rocket_contrib;
extern crate reqwest;
extern crate multipart;
extern crate ws;
extern crate rmpv;
extern crate chashmap;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
extern crate ring;
extern crate uuid;
extern crate chrono;
extern crate oath;
extern crate data_encoding;
extern crate jsonwebtoken as jwt;
extern crate u2f;
extern crate yubico;
extern crate dotenv;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate num_derive;
extern crate num_traits;
extern crate lettre;
extern crate lettre_email;
extern crate native_tls;
extern crate byteorder;
#[macro_use] extern crate rocket;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate serde_json;
#[macro_use] extern crate log;
#[macro_use] extern crate diesel;
#[macro_use] extern crate diesel_migrations;
#[macro_use] extern crate lazy_static;
#[macro_use] extern crate num_derive;
use std::{path::Path, process::{exit, Command}};
use rocket::Rocket;
@ -50,6 +24,9 @@ mod auth;
mod mail;
fn init_rocket() -> Rocket {
// TODO: TO HIDE MOUNTING LOG, call ignite, set logging to disabled, call all the mounts, and then enable it again
rocket::ignite()
.mount("/", api::web_routes())
.mount("/api", api::core_routes())
@ -69,7 +46,7 @@ mod migrations {
pub fn run_migrations() {
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
let connection = ::db::get_connection().expect("Can't conect to DB");
let connection = crate::db::get_connection().expect("Can't conect to DB");
use std::io::stdout;
embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations");
@ -77,6 +54,10 @@ mod migrations {
}
fn main() {
if CONFIG.extended_logging {
init_logging().ok();
}
check_db();
check_rsa_keys();
check_web_vault();
@ -85,13 +66,61 @@ fn main() {
init_rocket().launch();
}
fn init_logging() -> Result<(), fern::InitError> {
let mut logger = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(),
record.level(),
message
))
})
.level(log::LevelFilter::Debug)
.level_for("hyper", log::LevelFilter::Warn)
.level_for("ws", log::LevelFilter::Info)
.level_for("multipart", log::LevelFilter::Info)
.chain(std::io::stdout());
if let Some(log_file) = CONFIG.log_file.as_ref() {
logger = logger.chain(fern::log_file(log_file)?);
}
logger = chain_syslog(logger);
logger.apply()?;
Ok(())
}
#[cfg(not(feature = "enable_syslog"))]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch { logger }
#[cfg(feature = "enable_syslog")]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
let syslog_fmt = syslog::Formatter3164 {
facility: syslog::Facility::LOG_USER,
hostname: None,
process: "bitwarden_rs".into(),
pid: 0,
};
match syslog::unix(syslog_fmt) {
Ok(sl) => logger.chain(sl),
Err(e) => {
error!("Unable to connect to syslog: {:?}", e);
logger
}
}
}
fn check_db() {
let path = Path::new(&CONFIG.database_url);
if let Some(parent) = path.parent() {
use std::fs;
if fs::create_dir_all(parent).is_err() {
println!("Error creating database directory");
error!("Error creating database directory");
exit(1);
}
}
@ -106,16 +135,16 @@ fn check_rsa_keys() {
// If the RSA keys don't exist, try to create them
if !util::file_exists(&CONFIG.private_rsa_key)
|| !util::file_exists(&CONFIG.public_rsa_key) {
println!("JWT keys don't exist, checking if OpenSSL is available...");
info!("JWT keys don't exist, checking if OpenSSL is available...");
Command::new("openssl")
.arg("version")
.output().unwrap_or_else(|_| {
println!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH");
info!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH");
exit(1);
});
println!("OpenSSL detected, creating keys...");
info!("OpenSSL detected, creating keys...");
let mut success = Command::new("openssl").arg("genrsa")
.arg("-out").arg(&CONFIG.private_rsa_key_pem)
@ -139,9 +168,9 @@ fn check_rsa_keys() {
.status.success();
if success {
println!("Keys created correctly.");
info!("Keys created correctly.");
} else {
println!("Error creating keys, exiting...");
error!("Error creating keys, exiting...");
exit(1);
}
}
@ -155,7 +184,7 @@ fn check_web_vault() {
let index_path = Path::new(&CONFIG.web_vault_folder).join("index.html");
if !index_path.exists() {
println!("Web vault is not found. Please follow the steps in the README to install it");
error!("Web vault is not found. Please follow the steps in the README to install it");
exit(1);
}
}
@ -177,7 +206,7 @@ pub struct MailConfig {
impl MailConfig {
fn load() -> Option<Self> {
use util::{get_env, get_env_or};
use crate::util::{get_env, get_env_or};
// When SMTP_HOST is absent, we assume the user does not want to enable it.
let smtp_host = match get_env("SMTP_HOST") {
@ -186,7 +215,7 @@ impl MailConfig {
};
let smtp_from = get_env("SMTP_FROM").unwrap_or_else(|| {
println!("Please specify SMTP_FROM to enable SMTP support.");
error!("Please specify SMTP_FROM to enable SMTP support.");
exit(1);
});
@ -202,7 +231,7 @@ impl MailConfig {
let smtp_username = get_env("SMTP_USERNAME");
let smtp_password = get_env("SMTP_PASSWORD").or_else(|| {
if smtp_username.as_ref().is_some() {
println!("SMTP_PASSWORD is mandatory when specifying SMTP_USERNAME.");
error!("SMTP_PASSWORD is mandatory when specifying SMTP_USERNAME.");
exit(1);
} else {
None
@ -236,6 +265,9 @@ pub struct Config {
websocket_enabled: bool,
websocket_url: String,
extended_logging: bool,
log_file: Option<String>,
local_icon_extractor: bool,
signups_allowed: bool,
invitations_allowed: bool,
@ -256,7 +288,7 @@ pub struct Config {
impl Config {
fn load() -> Self {
use util::{get_env, get_env_or};
use crate::util::{get_env, get_env_or};
dotenv::dotenv().ok();
let df = get_env_or("DATA_FOLDER", "data".to_string());
@ -281,6 +313,9 @@ impl Config {
websocket_enabled: get_env_or("WEBSOCKET_ENABLED", false),
websocket_url: format!("{}:{}", get_env_or("WEBSOCKET_ADDRESS", "0.0.0.0".to_string()), get_env_or("WEBSOCKET_PORT", 3012)),
extended_logging: get_env_or("EXTENDED_LOGGING", true),
log_file: get_env("LOG_FILE"),
local_icon_extractor: get_env_or("LOCAL_ICON_EXTRACTOR", false),
signups_allowed: get_env_or("SIGNUPS_ALLOWED", true),

View file

@ -2,36 +2,46 @@
/// Macros
///
#[macro_export]
macro_rules! err {
($err:expr, $msg:expr) => {{
println!("ERROR: {}", $msg);
macro_rules! _err_object {
($msg:expr) => {{
err_json!(json!({
"error": $err,
"error_description": $err,
"ErrorModel": {
"Message": $msg,
"ValidationErrors": null,
"ExceptionMessage": null,
"ExceptionStackTrace": null,
"InnerExceptionMessage": null,
"Object": "error"
}}))
"Message": "",
"error": "",
"error_description": "",
"ValidationErrors": {"": [ $msg ]},
"ErrorModel": {
"Message": $msg,
"Object": "error"
},
"Object": "error"
}))
}};
($msg:expr) => { err!("unknown_error", $msg) }
}
#[macro_export]
macro_rules! err {
($msg:expr) => {{
error!("{}", $msg);
_err_object!($msg)
}};
($usr_msg:expr, $log_value:expr) => {{
error!("{}: {:#?}", $usr_msg, $log_value);
_err_object!($usr_msg)
}}
}
#[macro_export]
macro_rules! err_json {
($expr:expr) => {{
return Err($crate::rocket::response::status::BadRequest(Some($crate::rocket_contrib::Json($expr))));
return Err(rocket::response::status::BadRequest(Some(rocket_contrib::json::Json($expr))));
}}
}
#[macro_export]
macro_rules! err_handler {
($expr:expr) => {{
println!("ERROR: {}", $expr);
return $crate::rocket::Outcome::Failure(($crate::rocket::http::Status::Unauthorized, $expr));
error!("{}", $expr);
return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
}}
}
@ -92,6 +102,10 @@ pub fn get_display_size(size: i32) -> String {
format!("{} {}", size, UNITS[unit_counter])
}
pub fn get_uuid() -> String {
uuid::Uuid::new_v4().to_string()
}
///
/// String util methods
@ -238,6 +252,33 @@ fn upcase_value(value: &Value) -> Value {
fn _process_key(key: &str) -> String {
match key.to_lowercase().as_ref() {
"ssn" => "SSN".into(),
_ => self::upcase_first(key)
_ => self::upcase_first(key),
}
}
//
// Retry methods
//
pub fn retry<F, T, E>(func: F, max_tries: i32) -> Result<T, E>
where
F: Fn() -> Result<T, E>,
{
use std::{thread::sleep, time::Duration};
let mut tries = 0;
loop {
match func() {
ok @ Ok(_) => return ok,
err @ Err(_) => {
tries += 1;
if tries >= max_tries {
return err;
}
sleep(Duration::from_millis(500));
}
}
}
}