First working version

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

22
.dockerignore Normal file
View File

@ -0,0 +1,22 @@
# Local build artifacts
target
# Data folder
data
# IDE files
.vscode
.idea
*.iml
# Git and Docker files
.git
.gitignore
.gitmodules
Dockerfile
docker-compose.yml
.dockerignore
# Documentation
*.md

13
.env Normal file
View File

@ -0,0 +1,13 @@
# DATABASE_URL=data/db.sqlite3
# PRIVATE_RSA_KEY=data/private_rsa_key.der
# PUBLIC_RSA_KEY=data/public_rsa_key.der
# ICON_CACHE_FOLDER=data/icon_cache
# ATTACHMENTS_FOLDER=data/attachments
# true for yes, anything else for no
SIGNUPS_ALLOWED=true
# ROCKET_ENV=production
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
# ROCKET_PORT=8000
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
# Local build artifacts
target
# Data folder
data
# IDE files
.vscode
.idea
*.iml
# Environment file
# .env

1884
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

62
Cargo.toml Normal file
View File

@ -0,0 +1,62 @@
[package]
name = "bitwarden_rs"
version = "0.1.0"
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
[dependencies]
# Test framework, similar to rspec
stainless = "0.1.12"
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
rocket = { version = "0.3.6", features = ["tls"] }
rocket_codegen = "0.3.6"
rocket_contrib = "0.3.6"
# HTTP client
reqwest = "0.8.4"
# multipart/form-data support
multipart = "0.13.6"
# A generic serialization/deserialization framework
serde = "1.0.27"
serde_derive = "1.0.27"
serde_json = "1.0.9"
# A safe, extensible ORM and Query builder
# If tables need more than 16 columns, add feature "large-tables"
diesel = { version = "1.1.1", features = ["sqlite", "chrono"] }
diesel_migrations = {version = "1.1.0", features = ["sqlite"] }
# A generic connection pool
r2d2 = "0.8.2"
r2d2-diesel = "1.0.0"
# Crypto library
ring = { version = "0.11.0", features = ["rsa_signing"]}
# UUID generation
uuid = { version = "0.5.1", features = ["v4"] }
# Date and time library for Rust
chrono = "0.4.0"
time = "0.1.39"
# TOTP library
oath = "0.10.2"
# Data encoding library
data-encoding = "2.1.1"
# JWT library
jsonwebtoken = "4.0.0"
# A `dotenv` implementation for Rust
dotenv = { version = "0.10.1", default-features = false }
# Lazy static macro
lazy_static = "1.0.0"
[patch.crates-io]
jsonwebtoken = { path = "libs/jsonwebtoken" } # Make jwt use ring 0.11, to match rocket

62
Dockerfile Normal file
View File

@ -0,0 +1,62 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
FROM rustlang/rust:nightly as build
# Install the database libraries, in this case just sqlite3
RUN apt-get update && \
apt-get install -y sqlite3
# Install the diesel_cli tool, to manage migrations
# RUN cargo install diesel_cli --no-default-features --features sqlite
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app
WORKDIR /app
# Copies over *only* your manifests and vendored dependencies
COPY ./Cargo.* ./
COPY ./_libs ./_libs
# Builds your dependencies and removes the
# dummy project, except the target folder
RUN cargo build --release
RUN find . -not -path "./target*" -delete
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --release
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM debian:stretch-slim
# Install needed libraries
RUN apt-get update && \
apt-get install -y sqlite3 openssl libssl-dev
RUN mkdir /data
VOLUME /data
EXPOSE 80
# Copies the files from the context (migrations, web-vault, ...)
# and the binary from the "build" stage to the current stage
# TODO Only needs web-vault and .env
# COPY . .
COPY .env .
COPY web-vault ./web-vault
COPY --from=build app/target/release/bitwarden_rs .
# Configures the startup!
# Use production to disable Rocket logging
#CMD ROCKET_ENV=production ./bitwarden_rs
CMD ROCKET_ENV=staging ./bitwarden_rs

97
README.md Normal file
View File

@ -0,0 +1,97 @@
## Easy setup (Docker)
Install Docker to your system and then, from the project root, run:
```
# Build the docker image:
docker build -t dani/bitwarden_rs .
# Run the docker image with a docker volume:
docker volume create bw_data
docker run --name bitwarden_rs -it --init --rm --mount source=bw_data,target=/data -p 8000:80 dani/bitwarden_rs
# OR, Run the docker image with a host bind, where <absolute_path> is the absolute path to a folder in the host:
docker run --name bitwarden_rs -it --init --rm --mount type=bind,source=<absolute_path>,target=/data -p 8000:80 dani/bitwarden_rs
```
## How to compile bitwarden_rs
Install `rust nightly`, in Windows the recommended way is through `rustup`.
Install the `sqlite3`, and `openssl` libraries, in Windows the best option is Microsoft's `vcpkg`,
on other systems use their respective package managers.
Then run:
```
cargo run
# or
cargo build
```
## How to update the web-vault used
Install `node.js` and either `yarn` or `npm` (usually included with node)
Clone the web-vault outside the project:
```
git clone https://github.com/bitwarden/web.git web-vault
```
Modify `web-vault/settings.json` to look like this:
```json
{
"appSettings": {
"apiUri": "/api",
"identityUri": "/identity",
"iconsUri": "/icons",
"stripeKey": "",
"braintreeKey": ""
}
}
```
Then, run the following from the `web-vault` dir:
```
# With yarn (recommended)
yarn
yarn gulp dist:selfHosted
# With npm
npm install
npx gulp dist:selfHosted
```
Finally copy the contents of the `web-vault/dist` folder into the `bitwarden_rs/web-vault` folder.
## How to create the RSA signing key for JWT
Generate the RSA key:
```
openssl genrsa -out data/private_rsa_key.pem
```
Convert the generated key to .DER:
```
openssl rsa -in data/private_rsa_key.pem -outform DER -out data/private_rsa_key.der
```
And generate the public key:
```
openssl rsa -in data/private_rsa_key.der -inform DER -RSAPublicKey_out -outform DER -out data/public_rsa_key.der
```
## How to recreate database schemas
Install diesel-cli with cargo:
```
cargo install diesel_cli --no-default-features --features sqlite
```
Make sure that the correct path to the database is in the `.env` file.
If you want to modify the schemas, create a new migration with:
```
diesel migration generate <name>
```
Modify the *.sql files, making sure that any changes are reverted
in the down.sql file.
Apply the migrations and save the generated schemas as follows:
```
diesel migration redo
diesel print-schema > src/db/schema.rs
```

10
docker-compose.yml Normal file
View File

@ -0,0 +1,10 @@
## Docker Compose file, experimental and untested
# Run 'docker compose up' to start the service
version: '3'
services:
web:
build: .
ports:
- "8000:80"
volumes:
- ./data:/data

View File

@ -0,0 +1,20 @@
[package]
name = "jsonwebtoken"
version = "4.0.0"
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.8"
untrusted = "0.5"
chrono = "0.4"

21
libs/jsonwebtoken/LICENSE Normal file
View File

@ -0,0 +1,21 @@
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

@ -0,0 +1,120 @@
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

@ -0,0 +1,68 @@
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

@ -0,0 +1,64 @@
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

@ -0,0 +1,140 @@
//! Create and parses JWT (JSON Web Tokens)
//!
//! Documentation: [stable](https://docs.rs/jsonwebtoken/)
#![recursion_limit = "300"]
#![deny(missing_docs)]
#[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

@ -0,0 +1,42 @@
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))
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
DROP TABLE users;
DROP TABLE devices;
DROP TABLE ciphers;
DROP TABLE folders;

View File

@ -0,0 +1,50 @@
CREATE TABLE users (
uuid TEXT NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
password_hash BLOB NOT NULL,
salt BLOB NOT NULL,
password_iterations INTEGER NOT NULL,
password_hint TEXT,
key TEXT NOT NULL,
private_key TEXT,
public_key TEXT,
totp_secret TEXT,
totp_recover TEXT,
security_stamp TEXT NOT NULL
);
CREATE TABLE devices (
uuid TEXT NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
user_uuid TEXT NOT NULL REFERENCES users (uuid),
name TEXT NOT NULL,
type INTEGER NOT NULL,
push_token TEXT UNIQUE,
refresh_token TEXT UNIQUE NOT NULL
);
CREATE TABLE ciphers (
uuid TEXT NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
user_uuid TEXT NOT NULL REFERENCES users (uuid),
folder_uuid TEXT REFERENCES folders (uuid),
organization_uuid TEXT,
type INTEGER NOT NULL,
data TEXT NOT NULL,
favorite BOOLEAN NOT NULL,
attachments BLOB
);
CREATE TABLE folders (
uuid TEXT NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
user_uuid TEXT NOT NULL REFERENCES users (uuid),
name TEXT NOT NULL
);

149
src/api/core/accounts.rs Normal file
View File

@ -0,0 +1,149 @@
use rocket::Route;
use rocket::response::status::BadRequest;
use rocket_contrib::{Json, Value};
use db::DbConn;
use db::models::*;
use util;
use auth::Headers;
use CONFIG;
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct RegisterData {
email: String,
key: String,
keys: Option<KeysData>,
masterPasswordHash: String,
masterPasswordHint: Option<String>,
name: Option<String>,
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct KeysData {
encryptedPrivateKey: String,
publicKey: String,
}
#[post("/accounts/register", data = "<data>")]
fn register(data: Json<RegisterData>, conn: DbConn) -> Result<(), BadRequest<Json>> {
if CONFIG.signups_allowed {
err!(format!("Signups not allowed"))
}
println!("DEBUG - {:#?}", data);
if let Some(_) = User::find_by_mail(&data.email, &conn) {
err!("Email already exists")
}
let mut user = User::new(data.email.clone(),
data.key.clone(),
data.masterPasswordHash.clone());
// Add extra fields if present
if let Some(name) = data.name.clone() {
user.name = name;
}
if let Some(hint) = data.masterPasswordHint.clone() {
user.password_hint = Some(hint);
}
if let Some(ref keys) = data.keys {
user.private_key = Some(keys.encryptedPrivateKey.clone());
user.public_key = Some(keys.publicKey.clone());
}
user.save(&conn);
Ok(())
}
#[get("/accounts/profile")]
fn profile(headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
Ok(Json(headers.user.to_json()))
}
#[post("/accounts/keys", data = "<data>")]
fn post_keys(data: Json<KeysData>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let mut user = headers.user;
user.private_key = Some(data.encryptedPrivateKey.clone());
user.public_key = Some(data.publicKey.clone());
user.save(&conn);
Ok(Json(user.to_json()))
}
#[post("/accounts/password", data = "<data>")]
fn post_password(data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let key = data["key"].as_str().unwrap();
let password_hash = data["masterPasswordHash"].as_str().unwrap();
let new_password_hash = data["newMasterPasswordHash"].as_str().unwrap();
let mut user = headers.user;
if !user.check_valid_password(password_hash) {
err!("Invalid password")
}
user.set_password(new_password_hash);
user.key = key.to_string();
user.save(&conn);
Ok(Json(json!({})))
}
#[post("/accounts/security-stamp", data = "<data>")]
fn post_sstamp(data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let password_hash = data["masterPasswordHash"].as_str().unwrap();
let mut user = headers.user;
if !user.check_valid_password(password_hash) {
err!("Invalid password")
}
user.reset_security_stamp();
Ok(Json(json!({})))
}
#[post("/accounts/email-token", data = "<data>")]
fn post_email(data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
println!("{:#?}", data);
let password_hash = data["masterPasswordHash"].as_str().unwrap();
let mut user = headers.user;
if !user.check_valid_password(password_hash) {
err!("Invalid password")
}
err!("Not implemented")
}
#[post("/accounts/delete", data = "<data>")]
fn delete_account(data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let password_hash = data["masterPasswordHash"].as_str().unwrap();
let mut user = headers.user;
if !user.check_valid_password(password_hash) {
err!("Invalid password")
}
err!("Not implemented")
}
#[get("/accounts/revision-date")]
fn revision_date(headers: Headers, conn: DbConn) -> Result<String, BadRequest<Json>> {
let revision_date = headers.user.updated_at.timestamp();
Ok(revision_date.to_string())
}

251
src/api/core/ciphers.rs Normal file
View File

@ -0,0 +1,251 @@
use std::io::{Cursor, Read};
use rocket::{Route, Data};
use rocket::http::ContentType;
use rocket::response::status::BadRequest;
use rocket_contrib::{Json, Value};
use multipart::server::Multipart;
use db::DbConn;
use db::models::*;
use util;
use auth::Headers;
#[get("/sync")]
fn sync(headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let user = headers.user;
let folders = Folder::find_by_user(&user.uuid, &conn);
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect();
let ciphers = Cipher::find_by_user(&user.uuid, &conn);
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json()).collect();
Ok(Json(json!({
"Profile": user.to_json(),
"Folders": folders_json,
"Ciphers": ciphers_json,
"Domains": {
"EquivalentDomains": [],
"GlobalEquivalentDomains": [],
"Object": "domains",
},
"Object": "sync"
})))
}
#[get("/ciphers")]
fn get_ciphers(headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json()).collect();
Ok(Json(json!({
"Data": ciphers_json,
"Object": "list",
})))
}
#[get("/ciphers/<uuid>")]
fn get_cipher(uuid: String, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist")
};
if cipher.user_uuid != headers.user.uuid {
err!("Cipher is now owned by user")
}
Ok(Json(cipher.to_json()))
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct CipherData {
#[serde(rename = "type")]
type_: i32,
folderId: Option<String>,
organizationId: Option<String>,
name: Option<String>,
notes: Option<String>,
favorite: Option<bool>,
login: Option<Value>,
card: Option<Value>,
fields: Option<Vec<Value>>,
}
#[post("/ciphers", data = "<data>")]
fn post_ciphers(data: Json<CipherData>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let mut cipher = Cipher::new(headers.user.uuid.clone(),
data.type_,
data.favorite.unwrap_or(false));
if let Some(ref folder_id) = data.folderId {
// TODO: Validate folder is owned by user
cipher.folder_uuid = Some(folder_id.clone());
}
if let Some(ref org_id) = data.organizationId {
cipher.organization_uuid = Some(org_id.clone());
}
cipher.data = match value_from_data(&data) {
Ok(value) => {
use serde_json;
println!("--- {:?}", serde_json::to_string(&value));
println!("--- {:?}", value.to_string());
value.to_string()
}
Err(msg) => err!(msg)
};
cipher.save(&conn);
Ok(Json(cipher.to_json()))
}
fn value_from_data(data: &CipherData) -> Result<Value, &'static str> {
let mut values = json!({
"Name": data.name,
"Notes": data.notes
});
match data.type_ {
1 /*Login*/ => {
let login_data = match data.login {
Some(ref login) => login.clone(),
None => return Err("Login data missing")
};
if !copy_values(&login_data, &mut values) {
return Err("Login data invalid");
}
}
3 /*Card*/ => {
let card_data = match data.card {
Some(ref card) => card.clone(),
None => return Err("Card data missing")
};
if !copy_values(&card_data, &mut values) {
return Err("Card data invalid");
}
}
_ => return Err("Unknown type")
}
if let Some(ref fields) = data.fields {
values["Fields"] = Value::Array(fields.iter().map(|f| {
use std::collections::BTreeMap;
use serde_json;
let empty_map: BTreeMap<String, Value> = BTreeMap::new();
let mut value = serde_json::to_value(empty_map).unwrap();
copy_values(&f, &mut value);
value
}).collect());
} else {
values["Fields"] = Value::Null;
}
Ok(values)
}
fn copy_values(from: &Value, to: &mut Value) -> bool {
let map = match from.as_object() {
Some(map) => map,
None => return false
};
for (key, val) in map {
to[util::upcase_first(key)] = val.clone();
}
true
}
#[post("/ciphers/import", data = "<data>")]
fn post_ciphers_import(data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
println!("{:#?}", data);
err!("Not implemented")
}
#[post("/ciphers/<uuid>/attachment", format = "multipart/form-data", data = "<data>")]
fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
// TODO: Check if cipher exists
let mut params = content_type.params();
let boundary_pair = params.next().expect("No boundary provided"); // ("boundary", "----WebKitFormBoundary...")
let boundary = boundary_pair.1;
use data_encoding::BASE64URL;
use crypto;
use CONFIG;
// TODO: Maybe use the same format as the official server?
let attachment_id = BASE64URL.encode(&crypto::get_random_64());
let path = format!("{}/{}/{}", CONFIG.attachments_folder,
headers.user.uuid, attachment_id);
println!("Path {:#?}", path);
let mut mp = Multipart::with_body(data.open(), boundary);
match mp.save().with_dir(path).into_entries() {
Some(entries) => {
println!("Entries {:#?}", entries);
let saved_file = &entries.files["data"][0]; // Only one file at a time
let file_name = &saved_file.filename; // This is provided by the client, don't trust it
let file_size = &saved_file.size;
}
None => err!("No data entries")
}
err!("Not implemented")
}
#[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
fn delete_attachment(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
if uuid != headers.user.uuid {
err!("Permission denied")
}
// Delete file
// Delete entry in cipher
err!("Not implemented")
}
#[post("/ciphers/<uuid>")]
fn post_cipher(uuid: String, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
put_cipher(uuid, headers, conn)
}
#[put("/ciphers/<uuid>")]
fn put_cipher(uuid: String, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> { err!("Not implemented") }
#[delete("/ciphers/<uuid>")]
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> { err!("Not implemented") }
#[post("/ciphers/delete", data = "<data>")]
fn delete_all(data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let password_hash = data["masterPasswordHash"].as_str().unwrap();
let user = headers.user;
if !user.check_valid_password(password_hash) {
err!("Invalid password")
}
// Cipher::delete_from_user(&conn);
err!("Not implemented")
}

102
src/api/core/folders.rs Normal file
View File

@ -0,0 +1,102 @@
use rocket::Route;
use rocket::response::status::BadRequest;
use rocket_contrib::{Json, Value};
use db::DbConn;
use db::models::*;
use util;
use auth::Headers;
#[get("/folders")]
fn get_folders(headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect();
Ok(Json(json!({
"Data": folders_json,
"Object": "list",
})))
}
#[get("/folders/<uuid>")]
fn get_folder(uuid: String, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let mut folder = match Folder::find_by_uuid(&uuid, &conn) {
Some(folder) => folder,
_ => err!("Invalid folder")
};
if folder.user_uuid != headers.user.uuid {
err!("Folder belongs to another user")
}
Ok(Json(folder.to_json()))
}
#[post("/folders", data = "<data>")]
fn post_folders(data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let name = &data["name"].as_str();
if name.is_none() {
err!("Invalid name")
}
let folder = Folder::new(headers.user.uuid.clone(), name.unwrap().into());
folder.save(&conn);
Ok(Json(folder.to_json()))
}
#[post("/folders/<uuid>", data = "<data>")]
fn post_folder(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
put_folder(uuid, data, headers, conn)
}
#[put("/folders/<uuid>", data = "<data>")]
fn put_folder(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let mut folder = match Folder::find_by_uuid(&uuid, &conn) {
Some(folder) => folder,
_ => err!("Invalid folder")
};
if folder.user_uuid != headers.user.uuid {
err!("Folder belongs to another user")
}
let name = &data["name"].as_str();
if name.is_none() {
err!("Invalid name")
}
folder.name = name.unwrap().into();
folder.save(&conn);
Ok(Json(folder.to_json()))
}
#[post("/folders/<uuid>/delete", data = "<data>")]
fn delete_folder_post(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> Result<(), BadRequest<Json>> {
// Data contains a json object with the id, but we don't need it
delete_folder(uuid, headers, conn)
}
#[delete("/folders/<uuid>")]
fn delete_folder(uuid: String, headers: Headers, conn: DbConn) -> Result<(), BadRequest<Json>> {
let folder = match Folder::find_by_uuid(&uuid, &conn) {
Some(folder) => folder,
_ => err!("Invalid folder")
};
if folder.user_uuid != headers.user.uuid {
err!("Folder belongs to another user")
}
folder.delete(&conn);
Ok(())
}

100
src/api/core/mod.rs Normal file
View File

@ -0,0 +1,100 @@
mod accounts;
mod ciphers;
mod folders;
mod two_factor;
use self::accounts::*;
use self::ciphers::*;
use self::folders::*;
use self::two_factor::*;
pub fn routes() -> Vec<Route> {
routes![
register,
profile,
post_keys,
post_password,
post_sstamp,
post_email,
delete_account,
revision_date,
sync,
get_ciphers,
get_cipher,
post_ciphers,
post_ciphers_import,
post_attachment,
delete_attachment,
post_cipher,
put_cipher,
delete_cipher,
delete_all,
get_folders,
get_folder,
post_folders,
post_folder,
put_folder,
delete_folder_post,
delete_folder,
get_twofactor,
get_recover,
generate_authenticator,
activate_authenticator,
disable_authenticator,
get_collections,
clear_device_token,
put_device_token,
get_eq_domains,
post_eq_domains
]
}
///
/// Move this somewhere else
///
use rocket::Route;
use rocket::response::status::BadRequest;
use rocket_contrib::{Json, Value};
use db::DbConn;
use db::models::*;
use util;
use auth::Headers;
// GET /api/collections?writeOnly=false
#[get("/collections")]
fn get_collections() -> Result<Json, BadRequest<Json>> {
Ok(Json(json!({
"Data": [],
"Object": "list"
})))
}
#[put("/devices/identifier/<uuid>/clear-token")]
fn clear_device_token(uuid: String) -> Result<Json, BadRequest<Json>> { err!("Not implemented") }
#[put("/devices/identifier/<uuid>/token")]
fn put_device_token(uuid: String) -> Result<Json, BadRequest<Json>> { err!("Not implemented") }
#[get("/settings/domains")]
fn get_eq_domains() -> Result<Json, BadRequest<Json>> {
err!("Not implemented")
}
#[post("/settings/domains")]
fn post_eq_domains() -> Result<Json, BadRequest<Json>> {
err!("Not implemented")
}

131
src/api/core/two_factor.rs Normal file
View File

@ -0,0 +1,131 @@
use rocket::Route;
use rocket::response::status::BadRequest;
use rocket_contrib::{Json, Value};
use data_encoding::BASE32;
use db::DbConn;
use db::models::*;
use util;
use crypto;
use auth::Headers;
#[get("/two-factor")]
fn get_twofactor(headers: Headers) -> Result<Json, BadRequest<Json>> {
let data = if headers.user.totp_secret.is_none() {
Value::Null
} else {
json!([{
"Enabled": true,
"Type": 0,
"Object": "twoFactorProvider"
}])
};
Ok(Json(json!({
"Data": data,
"Object": "list"
})))
}
#[post("/two-factor/get-recover", data = "<data>")]
fn get_recover(data: Json<Value>, headers: Headers) -> Result<Json, BadRequest<Json>> {
let password_hash = data["masterPasswordHash"].as_str().unwrap();
if !headers.user.check_valid_password(password_hash) {
err!("Invalid password");
}
Ok(Json(json!({
"Code": headers.user.totp_recover,
"Object": "twoFactorRecover"
})))
}
#[post("/two-factor/get-authenticator", data = "<data>")]
fn generate_authenticator(data: Json<Value>, headers: Headers) -> Result<Json, BadRequest<Json>> {
let password_hash = data["masterPasswordHash"].as_str().unwrap();
if !headers.user.check_valid_password(password_hash) {
err!("Invalid password");
}
let (enabled, key) = match headers.user.totp_secret {
Some(secret) => (true, secret),
_ => (false, BASE32.encode(&crypto::get_random(vec![0u8; 20])))
};
Ok(Json(json!({
"Enabled": enabled,
"Key": key,
"Object": "twoFactorAuthenticator"
})))
}
#[post("/two-factor/authenticator", data = "<data>")]
fn activate_authenticator(data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let password_hash = data["masterPasswordHash"].as_str().unwrap();
if !headers.user.check_valid_password(password_hash) {
err!("Invalid password");
}
let token = data["token"].as_str(); // 123456
let key = data["key"].as_str().unwrap(); // YI4SKBIXG32LOA6VFKH2NI25VU3E4QML
// Validate key as base32 and 20 bytes length
let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {
Ok(decoded) => decoded,
_ => err!("Invalid totp secret")
};
if decoded_key.len() != 20 {
err!("Invalid key length")
}
// Set key in user.totp_secret
let mut user = headers.user;
user.totp_secret = Some(key.to_uppercase());
// Validate the token provided with the key
if !user.check_totp_code(util::parse_option_string(token)) {
err!("Invalid totp code")
}
// Generate totp_recover
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
user.totp_recover = Some(totp_recover);
user.save(&conn);
Ok(Json(json!({
"Enabled": true,
"Key": key,
"Object": "twoFactorAuthenticator"
})))
}
#[post("/two-factor/disable", data = "<data>")]
fn disable_authenticator(data: Json<Value>, headers: Headers, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let _type = &data["type"];
let password_hash = data["masterPasswordHash"].as_str().unwrap();
if !headers.user.check_valid_password(password_hash) {
err!("Invalid password");
}
let mut user = headers.user;
user.totp_secret = None;
user.totp_recover = None;
user.save(&conn);
Ok(Json(json!({
"Enabled": false,
"Type": 0,
"Object": "twoFactorProvider"
})))
}

85
src/api/icons.rs Normal file
View File

@ -0,0 +1,85 @@
use std::io;
use std::io::prelude::*;
use std::fs::{create_dir_all, File};
use std::path::Path;
use rocket::Route;
use rocket::response::Content;
use rocket::http::ContentType;
use reqwest;
use CONFIG;
pub fn routes() -> Vec<Route> {
routes![icon]
}
#[get("/<domain>/icon.png")]
fn icon(domain: String) -> Content<Vec<u8>> {
// Validate the domain to avoid directory traversal attacks
if domain.contains("/") || domain.contains("..") {
return Content(ContentType::PNG, get_fallback_icon());
}
let url = format!("https://icons.bitwarden.com/{}/icon.png", domain);
// Get the icon, or fallback in case of error
let icon = match get_icon_cached(&domain, &url) {
Ok(icon) => icon,
Err(e) => return Content(ContentType::PNG, get_fallback_icon())
};
Content(ContentType::PNG, icon)
}
fn get_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
let mut res = reqwest::get(url)?;
res = match res.error_for_status() {
Err(e) => return Err(e),
Ok(res) => res
};
let mut buffer: Vec<u8> = vec![];
res.copy_to(&mut buffer)?;
Ok(buffer)
}
fn get_icon_cached(key: &str, url: &str) -> io::Result<Vec<u8>> {
create_dir_all(&CONFIG.icon_cache_folder)?;
let path = &format!("{}/{}.png", CONFIG.icon_cache_folder, key);
/// Try to read the cached icon, and return it if it exists
match File::open(path) {
Ok(mut f) => {
let mut buffer = Vec::new();
if f.read_to_end(&mut buffer).is_ok() {
return Ok(buffer);
}
/* If error reading file continue */
}
Err(_) => { /* Continue */ }
}
println!("Downloading icon for {}...", key);
let icon = match get_icon(url) {
Ok(icon) => icon,
Err(_) => return Err(io::Error::new(io::ErrorKind::NotFound, ""))
};
/// Save the currently downloaded icon
match File::create(path) {
Ok(mut f) => { f.write_all(&icon); }
Err(_) => { /* Continue */ }
};
Ok(icon)
}
fn get_fallback_icon() -> Vec<u8> {
let fallback_icon = "https://raw.githubusercontent.com/bitwarden/web/master/src/images/fa-globe.png";
get_icon_cached("default", fallback_icon).unwrap()
}

225
src/api/identity.rs Normal file
View File

@ -0,0 +1,225 @@
use std::collections::HashMap;
use rocket::Route;
use rocket::request::{Form, FormItems, FromForm};
use rocket::response::status::BadRequest;
use rocket_contrib::Json;
use db::DbConn;
use db::models::*;
use util;
pub fn routes() -> Vec<Route> {
routes![ login]
}
#[post("/connect/token", data = "<connect_data>")]
fn login(connect_data: Form<ConnectData>, conn: DbConn) -> Result<Json, BadRequest<Json>> {
let data = connect_data.get();
println!("{:#?}", data);
let mut device = match data.grant_type {
GrantType::RefreshToken => {
// Extract token
let token = data.get("refresh_token").unwrap();
// Get device by refresh token
match Device::find_by_refresh_token(token, &conn) {
Some(device) => device,
None => err!("Invalid refresh token")
}
}
GrantType::Password => {
// Validate scope
let scope = data.get("scope").unwrap();
if scope != "api offline_access" {
err!("Scope not supported")
}
// Get the user
let username = data.get("username").unwrap();
let user = match User::find_by_mail(username, &conn) {
Some(user) => user,
None => err!("Invalid username or password")
};
// Check password
let password = data.get("password").unwrap();
if !user.check_valid_password(password) {
err!("Invalid username or password")
}
/*
//TODO: When invalid username or password, return this with a 400 BadRequest:
{
"error": "invalid_grant",
"error_description": "invalid_username_or_password",
"ErrorModel": {
"Message": "Username or password is incorrect. Try again.",
"ValidationErrors": null,
"ExceptionMessage": null,
"ExceptionStackTrace": null,
"InnerExceptionMessage": null,
"Object": "error"
}
}
*/
// Check if totp code is required and the value is correct
let totp_code = util::parse_option_string(data.get("twoFactorToken").map(String::as_ref));
if !user.check_totp_code(totp_code) {
// Return error 400
return err_json!(json!({
"error" : "invalid_grant",
"error_description" : "Two factor required.",
"TwoFactorProviders" : [ 0 ],
"TwoFactorProviders2" : { "0" : null }
}));
}
// Let's only use the header and ignore the 'devicetype' parameter
// TODO Get header Device-Type
let device_type_num = 0;// headers.device_type;
let (device_id, device_name) = match data.get("client_id").unwrap().as_ref() {
"web" => { (format!("web-{}", user.uuid), String::from("web")) }
"browser" | "mobile" => {
(
data.get("deviceidentifier").unwrap().clone(),
data.get("devicename").unwrap().clone(),
)
}
_ => err!("Invalid client id")
};
// Find device or create new
let device = match Device::find_by_uuid(&device_id, &conn) {
Some(device) => {
// Check if valid device
if device.user_uuid != user.uuid {
device.delete(&conn);
err!("Device is not owned by user")
}
device
}
None => {
// Create new device
Device::new(device_id, user.uuid, device_name, device_type_num)
}
};
device
}
};
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
let (access_token, expires_in) = device.refresh_tokens(&user);
device.save(&conn);
// TODO: when to include :privateKey and :TwoFactorToken?
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
})))
}
#[derive(Debug)]
struct ConnectData {
grant_type: GrantType,
data: HashMap<String, String>,
}
impl ConnectData {
fn get(&self, key: &str) -> Option<&String> {
self.data.get(&key.to_lowercase())
}
}
#[derive(Debug, Copy, Clone)]
enum GrantType { RefreshToken, Password }
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 {
let decoded_key: String = match key.url_decode() {
Ok(decoded) => decoded,
Err(e) => return Err(format!("Error decoding key: {}", value)),
};
let decoded_value: String = match value.url_decode() {
Ok(decoded) => decoded,
Err(e) => return Err(format!("Error decoding value: {}", value)),
};
data.insert(decoded_key.to_lowercase(), decoded_value);
}
// Validate needed values
let grant_type =
match data.get("grant_type").map(|s| &s[..]) {
Some("refresh_token") => {
// Check if refresh token is proviced
if let Err(msg) = check_values(&data, &VALUES_REFRESH) {
return Err(msg);
}
GrantType::RefreshToken
}
Some("password") => {
// Check if basic values are provided
if let Err(msg) = check_values(&data, &VALUES_PASSWORD) {
return Err(msg);
}
// Check that device values are present on device
match data.get("client_id").unwrap().as_ref() {
"browser" | "mobile" => {
if let Err(msg) = check_values(&data, &VALUES_DEVICE) {
return Err(msg);
}
}
_ => {}
}
GrantType::Password
}
_ => return Err(format!("Grant type not supported"))
};
Ok(ConnectData { grant_type, data })
}
}
fn check_values(map: &HashMap<String, String>, values: &[&str]) -> Result<(), String> {
for value in values {
if !map.contains_key(*value) {
return Err(format!("{} cannot be blank", value));
}
}
Ok(())
}

9
src/api/mod.rs Normal file
View File

@ -0,0 +1,9 @@
mod core;
mod icons;
mod identity;
mod web;
pub use self::core::routes as core_routes;
pub use self::icons::routes as icons_routes;
pub use self::identity::routes as identity_routes;
pub use self::web::routes as web_routes;

43
src/api/web.rs Normal file
View File

@ -0,0 +1,43 @@
use std::io;
use std::path::{Path, PathBuf};
use rocket::Route;
use rocket::response::NamedFile;
use rocket_contrib::{Json, Value};
use auth::Headers;
use CONFIG;
pub fn routes() -> Vec<Route> {
routes![index, files, attachments, alive]
}
// TODO: Might want to use in memory cache: https://github.com/hgzimmerman/rocket-file-cache
#[get("/")]
fn index() -> io::Result<NamedFile> {
NamedFile::open(Path::new(&CONFIG.web_vault_folder).join("index.html"))
}
#[get("/<p..>")] // Only match this if the other routes don't match
fn files(p: PathBuf) -> io::Result<NamedFile> {
NamedFile::open(Path::new(&CONFIG.web_vault_folder).join(p))
}
#[get("/attachments/<uuid>/<file..>")]
fn attachments(uuid: String, file: PathBuf, headers: Headers) -> io::Result<NamedFile> {
if uuid != headers.user.uuid {
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied"));
}
NamedFile::open(Path::new(&CONFIG.attachments_folder).join(file))
}
#[get("/alive")]
fn alive() -> Json<String> {
use util::format_date;
use chrono::{NaiveDateTime, Utc};
Json(format_date(&Utc::now().naive_utc()))
}

164
src/auth.rs Normal file
View File

@ -0,0 +1,164 @@
///
/// JWT Handling
///
use util::read_file;
use std::path::Path;
use time::Duration;
use jwt;
use serde::ser::Serialize;
use serde::de::Deserialize;
use CONFIG;
const JWT_ALGORITHM: jwt::Algorithm = jwt::Algorithm::RS256;
pub const JWT_ISSUER: &'static str = "localhost:8000/identity";
lazy_static! {
pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2);
static ref JWT_HEADER: jwt::Header = jwt::Header::new(JWT_ALGORITHM);
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key) {
Ok(key) => key,
Err(e) => panic!("Error loading private RSA Key from {}\n Error: {}", CONFIG.private_rsa_key, e)
};
static ref PUBLIC_RSA_KEY: Vec<u8> = match read_file(&CONFIG.public_rsa_key) {
Ok(key) => key,
Err(e) => panic!("Error loading public RSA Key from {}\n Error: {}", CONFIG.public_rsa_key, e)
};
}
pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
match jwt::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
Ok(token) => return token,
Err(e) => panic!("Error encoding jwt {}", e)
};
}
pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
let validation = jwt::Validation {
leeway: 30, // 30 seconds
validate_exp: true,
validate_iat: true,
validate_nbf: true,
aud: None,
iss: Some(JWT_ISSUER.into()),
sub: None,
algorithms: vec![JWT_ALGORITHM],
};
match jwt::decode(token, &PUBLIC_RSA_KEY, &validation) {
Ok(decoded) => Ok(decoded.claims),
Err(msg) => {
println!("Error validating jwt - {:#?}", msg);
Err(msg.to_string())
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JWTClaims {
// Not before
pub nbf: i64,
// Expiration time
pub exp: i64,
// Issuer
pub iss: String,
// Subject
pub sub: String,
pub premium: bool,
pub name: String,
pub email: String,
pub email_verified: bool,
// user security_stamp
pub sstamp: String,
// device uuid
pub device: String,
// [ "api", "offline_access" ]
pub scope: Vec<String>,
// [ "Application" ]
pub amr: Vec<String>,
}
///
/// Bearer token authentication
///
use rocket::Outcome;
use rocket::http::Status;
use rocket::request::{self, Request, FromRequest};
use db::DbConn;
use db::models::{User, Device};
pub struct Headers {
pub device_type: i32,
pub device: Device,
pub user: User,
}
impl<'a, 'r> FromRequest<'a, 'r> for Headers {
type Error = &'static str;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let headers = request.headers();
/// Get device type
let device_type = match headers.get_one("Device-Type")
.map(|s| s.parse::<i32>()) {
Some(Ok(dt)) => dt,
_ => return err_handler!("Device-Type is invalid or missing")
};
/// Get access_token
let access_token: &str = match request.headers().get_one("Authorization") {
Some(a) => {
let split: Option<&str> = a.rsplit("Bearer ").next();
if split.is_none() {
err_handler!("No access token provided")
}
split.unwrap()
}
None => err_handler!("No access token provided")
};
/// Check JWT token is valid and get device and user from it
let claims: JWTClaims = match decode_jwt(access_token) {
Ok(claims) => claims,
Err(msg) => {
println!("Invalid claim: {}", msg);
err_handler!("Invalid claim")
}
};
let device_uuid = claims.device;
let user_uuid = claims.sub;
let conn = match request.guard::<DbConn>() {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB")
};
let device = match Device::find_by_uuid(&device_uuid, &conn) {
Some(device) => device,
None => err_handler!("Invalid device id")
};
let user = match User::find_by_uuid(&user_uuid, &conn) {
Some(user) => user,
None => err_handler!("Device has no user associated")
};
if user.security_stamp != claims.sstamp {
err_handler!("Invalid security stamp")
}
Outcome::Success(Headers { device_type, device, user })
}
}

168
src/bin/proxy.rs Normal file
View File

@ -0,0 +1,168 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
extern crate rocket_contrib;
extern crate reqwest;
use std::io::{self, Cursor};
use std::str::FromStr;
use std::path::{Path, PathBuf};
use rocket::{Request, Response};
use rocket::config::Config;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http;
use rocket::response::NamedFile;
use reqwest::header::{self, Headers};
/**
** These routes are here to avoid showing errors in the console,
** redirect the body data to the fairing and show the web vault.
**/
#[get("/")]
fn index() -> io::Result<NamedFile> {
NamedFile::open(Path::new("web-vault").join("index.html"))
}
#[get("/<p..>")] // Only match this if the other routes don't match
fn get(p: PathBuf) -> io::Result<NamedFile> {
NamedFile::open(Path::new("web-vault").join(p))
}
#[delete("/<_p..>")]
fn delete(_p: PathBuf) {}
#[put("/<_p..>", data = "<d>")]
fn put(_p: PathBuf, d: Vec<u8>) -> Vec<u8> { d }
#[post("/<_p..>", data = "<d>")]
fn post(_p: PathBuf, d: Vec<u8>) -> Vec<u8> { d }
fn main() {
let config = Config::development().unwrap();
rocket::custom(config, false)
.mount("/", routes![get, put, post, delete, index])
.attach(ProxyFairing { client: reqwest::Client::new() })
.launch();
}
struct ProxyFairing {
client: reqwest::Client
}
impl Fairing for ProxyFairing {
fn info(&self) -> Info {
Info {
name: "Proxy Fairing",
kind: Kind::Launch | Kind::Response,
}
}
fn on_launch(&self, _rocket: &rocket::Rocket) {
println!("Started proxy on locahost:8000");
}
fn on_response(&self, req: &Request, res: &mut Response) {
// Prepare the data to make the request
// -------------------------------------
let url = {
let url = req.uri().as_str();
// Check if we are outside the API paths
if !url.starts_with("/api/")
&& !url.starts_with("/identity/") {
return;
}
// Replace the path with the real server URL
url.replacen("/api/", "https://api.bitwarden.com/", 1)
.replacen("/identity/", "https://identity.bitwarden.com/", 1)
};
let host = url.split("/").collect::<Vec<_>>()[2];
let headers = headers_rocket_to_reqwest(req.headers(), host);
let method = reqwest::Method::from_str(req.method().as_str()).unwrap();
let body = res.body_bytes();
println!("\n\nREQ. {} {}", req.method().as_str(), url);
println!("HEADERS. {:#?}", headers);
if let Some(ref body) = body {
let body_string = String::from_utf8_lossy(body);
if !body_string.contains("<!DOCTYPE html>") {
println!("BODY. {:?}", body_string);
}
}
// Execute the request
// -------------------------------------
let mut client = self.client.request(method, &url);
let request_builder = client.headers(headers);
if let Some(body_vec) = body {
request_builder.body(body_vec);
}
let mut server_res = match request_builder.send() {
Ok(response) => response,
Err(e) => {
res.set_status(http::Status::BadRequest);
res.set_sized_body(Cursor::new(e.to_string()));
return;
}
};
// Get the response values
// -------------------------------------
let mut res_body: Vec<u8> = vec![];
server_res.copy_to(&mut res_body).unwrap();
let res_status = server_res.status().as_u16();
let mut res_headers = server_res.headers().clone();
// These headers break stuff
res_headers.remove::<header::TransferEncoding>();
res_headers.remove::<header::ContentLength>();
println!("\n\nRES. {} {}", res_status, url);
// Nothing interesting here
// println!("HEADERS. {:#?}", res_headers);
println!("BODY. {:?}", String::from_utf8_lossy(&res_body));
// Prepare the response
// -------------------------------------
res.set_status(http::Status::from_code(res_status).unwrap_or(http::Status::BadRequest));
headers_reqwest_to_rocket(&res_headers, res);
res.set_sized_body(Cursor::new(res_body));
}
}
fn headers_rocket_to_reqwest(headers: &http::HeaderMap, host: &str) -> Headers {
let mut new_headers = Headers::new();
for header in headers.iter() {
let name = header.name().to_string();
let value = if name.to_lowercase() != "host" {
header.value().to_string()
} else {
host.to_string()
};
new_headers.set_raw(name, value);
}
new_headers
}
fn headers_reqwest_to_rocket(headers: &Headers, res: &mut Response) {
for header in headers.iter() {
res.set_raw_header(header.name().to_string(), header.value_string());
}
}

36
src/crypto.rs Normal file
View File

@ -0,0 +1,36 @@
///
/// PBKDF2 derivation
///
use ring::{digest, pbkdf2};
static DIGEST_ALG: &digest::Algorithm = &digest::SHA256;
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
pub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
let mut out = vec![0u8; OUTPUT_LEN]; // Initialize array with zeros
pbkdf2::derive(DIGEST_ALG, iterations, salt, secret, &mut out);
out
}
pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterations: u32) -> bool {
pbkdf2::verify(DIGEST_ALG, iterations, salt, secret, previous).is_ok()
}
///
/// Random values
///
pub fn get_random_64() -> Vec<u8> {
get_random(vec![0u8; 64])
}
pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
use ring::rand::{SecureRandom, SystemRandom};
SystemRandom::new().fill(&mut array);
array
}

Some files were not shown because too many files have changed in this diff Show More