Skip to content

Commit

Permalink
encrypt refresh tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
carderne committed Apr 16, 2024
1 parent 03060d6 commit a2a4ce7
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 17 deletions.
34 changes: 34 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ diesel = { version = "2.1.0", features = ["sqlite"] }
diesel_migrations = "2.1.0"
dotenvy = "0.15"
env_logger = "0.11.3"
fernet = "0.2.1"
geo = { version = "0.28.0", features = ["serde"] }
geo-types = "0.7.13"
geojson = "0.24.1"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Running on [render.com](https://render.com/) with a local SQLite DB on a persist
## Development
You'll need to create a `.env` file with the following:
```bash
RUST_LOG=info
FERNET_KEYS='32-bytes-of-base64-encoded,another-one-for-rotation'
ROCKET_DATABASES='{db={url="db.sqlite"}}'
ROCKET_SECRET_KEY=''
REDIRECT_URI='http://localhost:8000/callback'
Expand Down
74 changes: 74 additions & 0 deletions src/crypto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use fernet::{Fernet, MultiFernet};
use std::env;

use crate::error;

pub struct Crypto {
mf: MultiFernet,
}

impl Default for Crypto {
fn default() -> Self {
let keys = env::var("FERNET_KEYS").unwrap();
Self::new(&keys)
}
}

impl Crypto {
pub fn new(keys: &str) -> Self {
let keys: Vec<&str> = keys.split(',').collect();
let fernets: Vec<Fernet> = keys.iter().map(|k| Fernet::new(k).unwrap()).collect();
Self {
mf: MultiFernet::new(fernets),
}
}

pub fn encrypt(&self, data: &str) -> String {
let data = data.as_bytes().to_vec();
self.mf.encrypt(&data)
}

pub fn decrypt(&self, data: &str) -> Result<String, error::Error> {
let out = self.mf.decrypt(data)?;
let out = String::from_utf8(out)?;
Ok(out)
}

/// Will simply return the data if decryption fails
/// so that things continue to work during migration to encryption
pub fn decrypt_fallback(&self, data: &str) -> String {
let decrypted = self.decrypt(data);
match decrypted {
Ok(out) => out,
Err(_) => data.to_string(),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_encrypt_decrypt() {
let k1 = Fernet::generate_key();
let k2 = Fernet::generate_key();
let keys = format!("{},{}", k1, k2);
let c = Crypto::new(&keys);
let want = "longrandombunchoftokenstuff12345ABCDEFG";
let encrypted = c.encrypt(want);
let got = c.decrypt(&encrypted).unwrap();
assert_eq!(want, got);
}

#[test]
fn test_decrypt_fallback() {
let k1 = Fernet::generate_key();
let k2 = Fernet::generate_key();
let keys = format!("{},{}", k1, k2);
let c = Crypto::new(&keys);
let want = "longrandombunchoftokenstuff12345ABCDEFG";
let got = c.decrypt_fallback(want);
assert_eq!(want, got);
}
}
41 changes: 28 additions & 13 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use log::{debug, warn};
use rocket::{Build, Rocket};
use rocket_sync_db_pools::database;

use crate::crypto::Crypto;
use crate::error;
use crate::models::UserDb;
use crate::schema::users::dsl::*;
Expand All @@ -32,9 +33,10 @@ pub async fn migrate(rocket: Rocket<Build>) -> Result<Rocket<Build>, Rocket<Buil
}

pub async fn save_user(db: &Db, t: &strava::TokenResponse) -> Result<usize, error::Error> {
let ref_token = Crypto::default().encrypt(&t.refresh_token);
let user = UserDb {
id: t.athlete.id,
refresh_token: t.refresh_token.clone(),
refresh_token: ref_token,
access_token: t.access_token.clone(),
expires_at: t.expires_at,
};
Expand All @@ -57,29 +59,42 @@ pub async fn save_user(db: &Db, t: &strava::TokenResponse) -> Result<usize, erro
}

pub async fn get_user(db: &Db, user_id: i32) -> Result<UserDb, error::Error> {
db.run(move |c| {
users
.find(user_id)
.select(UserDb::as_select())
.first(c)
.with_context(|| "db::get_user".to_string())
.map_err(error::Error::from)
})
.await
let user = db
.run(move |c| {
users
.find(user_id)
.select(UserDb::as_select())
.first(c)
.with_context(|| "db::get_user".to_string())
.map_err(error::Error::from)
})
.await?;
let ref_token = Crypto::default().decrypt_fallback(&user.refresh_token);
let user = UserDb {
id: user.id,
access_token: user.access_token,
refresh_token: ref_token,
expires_at: user.expires_at,
};
Ok(user)
}

/// These pragmas hopefully prevent the DB from locking up
/// Source: https://github.com/the-lean-crate/criner/issues/1
pub async fn prep_db(db: &Db) -> Result<(), error::Error> {
db.run(|c| {
c.batch_execute("
c.batch_execute(
"
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA wal_autocheckpoint = 100;
PRAGMA wal_checkpoint(TRUNCATE);
").map_err(|err| {
",
)
.map_err(|err| {
warn!("Failed to prep db");
error::Error::from(err)
})
}).await
})
.await
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! Playing around with the Strava API

pub mod crypto;
pub mod db;
pub mod error;
pub mod geo;
Expand Down
3 changes: 1 addition & 2 deletions src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ async fn get_data(conn: Db, user: User) -> Result<Json<Data>, error::Error> {
// which was quite elegant, but didn't provide for refreshing...
let token = if expired {
// get a new token (using refresh_token) if this one expired

info!("getting new refresh token for id {}", id);
info!("getting new token for id {}", id);
let token_response = strava::StravaClient::default()
.get_token(&user.refresh_token, strava::GrantType::Refresh)
.await?;
Expand Down
6 changes: 4 additions & 2 deletions static/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ export const fetchData = (map) => {
$("legend").style.display = "none";
if (res.status === 401) {
$("error401").style.display = "flex";
} else if (res.status === 503) {
$("error503").style.display = "flex";
} else {
$("error500").style.display = "flex";
}
Expand All @@ -214,9 +216,9 @@ export const fetchData = (map) => {
if (err.message !== "backend") {
$("legend").style.display = "none";
$("error500").style.display = "flex";
throw new Error("failed to parse backend data", err);
console.err("failed to parse backend data", err);
} else {
throw err;
console.err("backend error", err);
}
})
.finally(() => {
Expand Down
11 changes: 11 additions & 0 deletions templates/index.html.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@
</div>
</div>

<div id="error503" role="status" class="fixed inset-0 flex justify-center items-center z-50" style="display:none">
<div class="bg-white/90 p-8 rounded-lg shadow-lg">
<p>Couldn't find your user, try logging in again</p>
<div class="flex justify-center mt-4">
<a href="/auth" class="inline-block cursor-pointer">
<img src="/static/strava-button.png" alt="Connect with Strava">
</a>
</div>
</div>
</div>

<div id="error500" role="status" class="fixed inset-0 flex justify-center items-center z-50" style="display:none">
<div class="bg-white/90 p-8 rounded-lg shadow-lg justify-center">
<p class="text-center">Unknown error, try reloading in a minute!</p>
Expand Down

0 comments on commit a2a4ce7

Please sign in to comment.