use uuid::Uuid; use bcrypt::{hash, verify}; use rand::{thread_rng, Rng}; use rand::distributions::Alphanumeric; use std::iter; use serde_cbor::{from_slice}; use postgres::transaction::Transaction; use rpc::{AccountCreateParams, AccountLoginParams}; use cryp::{Cryp, CrypRecover, cryp_write}; use game::Game; use zone::{Zone, zone_delete}; use failure::Error; use failure::err_msg; static PASSWORD_MIN_LEN: usize = 12; #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Account { pub id: Uuid, pub name: String, token: String, } #[derive(Debug,Clone,Serialize,Deserialize)] struct AccountEntry { id: Uuid, name: String, password: String, token: String, } // MAYBE // hash tokens with a secret pub fn account_from_token(token: String, tx: &mut Transaction) -> Result { let query = " SELECT id, name, token FROM accounts WHERE token = $1; "; let result = tx .query(query, &[&token])?; let returned = match result.iter().next() { Some(row) => row, None => return Err(err_msg("invalid token")), }; let entry = Account { id: returned.get(0), name: returned.get(1), token: returned.get(2), }; return Ok(entry); } pub fn account_create(params: AccountCreateParams, tx: &mut Transaction) -> Result { let id = Uuid::new_v4(); if params.password.len() < PASSWORD_MIN_LEN { return Err(err_msg("password must be at least 12 characters")); } let rounds = 8; let password = hash(¶ms.password, rounds)?; let mut rng = thread_rng(); let token: String = iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) .take(64) .collect(); let account = AccountEntry { name: params.name, id, password, token, }; let query = " INSERT INTO accounts (id, name, password, token) VALUES ($1, $2, $3, $4) RETURNING id, name, token; "; let result = tx .query(query, &[&account.id, &account.name, &account.password, &account.token])?; let returned = result.iter().next().expect("no row returned"); let entry = Account { id: returned.get(0), name: returned.get(1), token: returned.get(2), }; println!("{:?} registered", entry.name); return Ok(entry); } pub fn account_login(params: AccountLoginParams, tx: &mut Transaction) -> Result { let query = " SELECT id, name, token, password FROM accounts WHERE name = $1; "; let result = tx .query(query, &[¶ms.name])?; let returned = match result.iter().next() { Some(row) => row, // MAYBE // verify gibberish to delay response for timing attacks None => return Err(err_msg("account not found")), }; let entry = AccountEntry { id: returned.get(0), name: returned.get(1), token: returned.get(2), password: returned.get(3), }; if !verify(¶ms.password, &entry.password)? { return Err(err_msg("password does not match")); } println!("{:?} logged in", entry.name); // MAYBE // update token? // don't necessarily want to log every session out when logging in let account = Account { id: entry.id, name: entry.name, token: entry.token, }; return Ok(account); } fn recover_cryp(cryp_bytes: Vec, tx: &mut Transaction) -> Result { let c = from_slice::(&cryp_bytes)?; let mut cryp = Cryp::new() .named(&c.name) .level(c.lvl) .set_account(c.account) .create(); cryp.id = c.id; println!("recovered cryp {:?}", c.name); return cryp_write(cryp, tx); } pub fn account_cryps(tx: &mut Transaction, account: &Account) -> Result, Error> { let query = " SELECT data FROM cryps WHERE account = $1; "; let result = tx .query(query, &[&account.id])?; let cryps: Result, _> = result.iter() .map(|row| { let cryp_bytes: Vec = row.get(0); match from_slice::(&cryp_bytes) { Ok(c) => Ok(c), Err(_e) => recover_cryp(cryp_bytes, tx), } }) .collect(); // catch any errors if cryps.is_err() { return Err(err_msg("could not deserialize a cryp")); } // now unwrap is safe return Ok(cryps.unwrap()); } pub fn account_game_history(tx: &mut Transaction, account: &Account) -> Result, Error> { let query = " SELECT games.data FROM players join games ON (players.game = games.id) WHERE account = $1; "; let result = tx .query(query, &[&account.id])?; let games: Result, _> = result.iter().map(|row| { let cryp_bytes: Vec = row.get(0); from_slice::(&cryp_bytes) }).collect(); // catch any errors if games.is_err() { return Err(err_msg("could not deserialize a game")); } // now unwrap is safe return Ok(games.unwrap()); } pub fn account_zone(tx: &mut Transaction, account: &Account) -> Result { let query = " SELECT * FROM zones WHERE account = $1 AND active = true; "; let result = tx .query(query, &[&account.id])?; let returned = match result.iter().next() { Some(row) => row, None => return Err(err_msg("no active zone")), }; // tells from_slice to cast into a cryp let bytes: Vec = returned.get("data"); let zone = match from_slice::(&bytes) { Ok(z) => z, Err(_) => { zone_delete(tx, returned.get("id"))?; return Err(err_msg("invalid zone removed")) }, }; return Ok(zone); }