Aaaaaand they're encrypted now too

This commit is contained in:
Ellie Huxtable 2023-07-09 20:59:49 +01:00
parent 8f0eda013e
commit d8e7e2c9d2
13 changed files with 44 additions and 36 deletions

View File

@ -7,7 +7,8 @@ create table if not exists records (
timestamp integer not null,
tag text not null,
version text not null,
data blob not null
data blob not null,
cek blob not null
);
create index host_idx on records (host);

View File

@ -1,3 +0,0 @@
-- store content encryption keys in the record
alter table records
add column cek text;

View File

@ -9,7 +9,7 @@ use reqwest::{
StatusCode, Url,
};
use atuin_common::record::Record;
use atuin_common::record::{EncryptedData, Record};
use atuin_common::{
api::{
AddHistoryRequest, CountResponse, DeleteHistoryRequest, ErrorResponse, IndexResponse,
@ -200,7 +200,7 @@ impl<'a> Client<'a> {
Ok(())
}
pub async fn post_records(&self, records: &[Record]) -> Result<()> {
pub async fn post_records(&self, records: &[Record<EncryptedData>]) -> Result<()> {
let url = format!("{}/record", self.sync_addr);
let url = Url::parse(url.as_str())?;
@ -215,7 +215,7 @@ impl<'a> Client<'a> {
tag: String,
start: Option<Uuid>,
count: u64,
) -> Result<Vec<Record>> {
) -> Result<Vec<Record<EncryptedData>>> {
let url = format!(
"{}/record/next?host={}&tag={}&count={}",
self.sync_addr, host, tag, count
@ -230,7 +230,7 @@ impl<'a> Client<'a> {
let resp = self.client.get(url).send().await?;
let records = resp.json::<Vec<Record>>().await?;
let records = resp.json::<Vec<Record<EncryptedData>>>().await?;
Ok(records)
}

View File

@ -151,7 +151,7 @@ impl KvStore {
}
if let Some(parent) = decrypted.parent {
record = store.get(parent.as_str()).await?;
record = store.get(parent).await?;
} else {
break;
}

View File

@ -6,6 +6,7 @@ use rusty_paseto::core::{
ImplicitAssertion, Key as DataKey, Local as LocalPurpose, Paseto, PasetoNonce, Payload, V4,
};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Use PASETO V4 Local encryption using the additional data as an implicit assertion.
#[allow(non_camel_case_types)]
@ -158,10 +159,10 @@ struct AtuinFooter {
// This cannot be changed, otherwise it breaks the authenticated encryption.
#[derive(Debug, Copy, Clone, Serialize)]
struct Assertions<'a> {
id: &'a str,
id: &'a Uuid,
version: &'a str,
tag: &'a str,
host: &'a str,
host: &'a Uuid,
}
impl<'a> From<AdditionalData<'a>> for Assertions<'a> {

View File

@ -21,7 +21,7 @@ pub trait Store {
records: impl Iterator<Item = &Record<EncryptedData>> + Send + Sync,
) -> Result<()>;
async fn get(&self, id: &str) -> Result<Record<EncryptedData>>;
async fn get(&self, id: Uuid) -> Result<Record<EncryptedData>>;
async fn len(&self, host: Uuid, tag: &str) -> Result<u64>;
/// Get the record that follows this record

View File

@ -119,7 +119,6 @@ async fn sync_upload(
// remote tail = current local tail
let mut record = Some(store.get(start).await.unwrap());
record = store.next(&record.unwrap()).await?;
let mut buf = Vec::with_capacity(upload_page_size);

View File

@ -8,7 +8,7 @@ use uuid::Uuid;
#[derive(Clone, Debug, PartialEq)]
pub struct DecryptedData(pub Vec<u8>);
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EncryptedData {
pub data: String,
pub content_encryption_key: String,
@ -54,10 +54,10 @@ pub struct Record<Data> {
/// Extra data from the record that should be encoded in the data
#[derive(Debug, Copy, Clone)]
pub struct AdditionalData<'a> {
pub id: &'a str,
pub id: &'a Uuid,
pub version: &'a str,
pub tag: &'a str,
pub host: &'a str,
pub host: &'a Uuid,
}
impl<Data> Record<Data> {

View File

@ -13,7 +13,10 @@ use self::{
models::{History, NewHistory, NewSession, NewUser, Session, User},
};
use async_trait::async_trait;
use atuin_common::{record::Record, utils::get_days_from_month};
use atuin_common::{
record::{EncryptedData, Record},
utils::get_days_from_month,
};
use chrono::{Datelike, TimeZone};
use chronoutil::RelativeDuration;
use serde::{de::DeserializeOwned, Serialize};
@ -56,7 +59,7 @@ pub trait Database: Sized + Clone + Send + Sync + 'static {
async fn delete_history(&self, user: &User, id: String) -> DbResult<()>;
async fn deleted_history(&self, user: &User) -> DbResult<Vec<String>>;
async fn add_records(&self, user: &User, record: &[Record]) -> DbResult<()>;
async fn add_records(&self, user: &User, record: &[Record<EncryptedData>]) -> DbResult<()>;
async fn next_records(
&self,
user: &User,
@ -64,7 +67,7 @@ pub trait Database: Sized + Clone + Send + Sync + 'static {
tag: String,
start: Option<Uuid>,
count: u64,
) -> DbResult<Vec<Record>>;
) -> DbResult<Vec<Record<EncryptedData>>>;
// Return the tail record ID for each store, so (HostID, Tag, TailRecordID)
async fn tail_records(&self, user: &User) -> DbResult<Vec<(Uuid, String, Uuid)>>;

View File

@ -7,7 +7,8 @@ create table records (
timestamp bigint not null, -- not a timestamp type, as those do not have nanosecond precision
version text not null,
tag text not null, -- what is this? history, kv, whatever. Remember clients get a log per tag per host
data bytea not null, -- store the actual history data, encrypted. I don't wanna know!
data text not null, -- store the actual history data, encrypted. I don't wanna know!
cek text not null,
user_id bigint not null, -- allow multiple users
created_at timestamp not null default current_timestamp

View File

@ -1,7 +1,7 @@
use std::str::FromStr;
use async_trait::async_trait;
use atuin_common::record::Record;
use atuin_common::record::{EncryptedData, Record};
use atuin_server_database::models::{History, NewHistory, NewSession, NewUser, Session, User};
use atuin_server_database::{Database, DbError, DbResult};
use futures_util::TryStreamExt;
@ -335,7 +335,7 @@ impl Database for Postgres {
}
#[instrument(skip_all)]
async fn add_records(&self, user: &User, records: &[Record]) -> DbResult<()> {
async fn add_records(&self, user: &User, records: &[Record<EncryptedData>]) -> DbResult<()> {
let mut tx = self.pool.begin().await.map_err(fix_error)?;
for i in records {
@ -343,8 +343,8 @@ impl Database for Postgres {
sqlx::query(
"insert into records
(id, client_id, host, parent, timestamp, version, tag, data, user_id)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
(id, client_id, host, parent, timestamp, version, tag, data, cek, user_id)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
on conflict do nothing
",
)
@ -355,7 +355,8 @@ impl Database for Postgres {
.bind(i.timestamp as i64) // throwing away some data, but i64 is still big in terms of time
.bind(&i.version)
.bind(&i.tag)
.bind(&i.data)
.bind(&i.data.data)
.bind(&i.data.content_encryption_key)
.bind(user.id)
.execute(&mut tx)
.await
@ -375,7 +376,7 @@ impl Database for Postgres {
tag: String,
start: Option<Uuid>,
count: u64,
) -> DbResult<Vec<Record>> {
) -> DbResult<Vec<Record<EncryptedData>>> {
tracing::debug!("{:?} - {:?} - {:?}", host, tag, start);
let mut ret = Vec::with_capacity(count as usize);
let mut parent = start;
@ -402,7 +403,7 @@ impl Database for Postgres {
match record {
Ok(record) => {
let record: Record = record.into();
let record: Record<EncryptedData> = record.into();
ret.push(record.clone());
parent = Some(record.id);

View File

@ -1,12 +1,12 @@
use ::sqlx::{FromRow, Result};
use atuin_common::record::Record;
use atuin_common::record::{EncryptedData, Record};
use atuin_server_database::models::{History, Session, User};
use sqlx::{postgres::PgRow, Row};
pub struct DbUser(pub User);
pub struct DbSession(pub Session);
pub struct DbHistory(pub History);
pub struct DbRecord(pub Record);
pub struct DbRecord(pub Record<EncryptedData>);
impl<'a> FromRow<'a, PgRow> for DbUser {
fn from_row(row: &'a PgRow) -> Result<Self> {
@ -47,6 +47,11 @@ impl<'a> ::sqlx::FromRow<'a, PgRow> for DbRecord {
fn from_row(row: &'a PgRow) -> ::sqlx::Result<Self> {
let timestamp: i64 = row.try_get("timestamp")?;
let data = EncryptedData {
data: row.try_get("data")?,
content_encryption_key: row.try_get("cek")?,
};
Ok(Self(Record {
id: row.try_get("client_id")?,
host: row.try_get("host")?,
@ -54,13 +59,13 @@ impl<'a> ::sqlx::FromRow<'a, PgRow> for DbRecord {
timestamp: timestamp as u64,
version: row.try_get("version")?,
tag: row.try_get("tag")?,
data: row.try_get("data")?,
data,
}))
}
}
impl Into<Record> for DbRecord {
fn into(self) -> Record {
impl Into<Record<EncryptedData>> for DbRecord {
fn into(self) -> Record<EncryptedData> {
Record { ..self.0 }
}
}

View File

@ -8,13 +8,13 @@ use super::{ErrorResponse, ErrorResponseStatus, RespExt};
use crate::router::{AppState, UserAuth};
use atuin_server_database::Database;
use atuin_common::record::{Record, RecordIndex};
use atuin_common::record::{EncryptedData, Record, RecordIndex};
#[instrument(skip_all, fields(user.id = user.id))]
pub async fn post<DB: Database>(
UserAuth(user): UserAuth,
state: State<AppState<DB>>,
Json(records): Json<Vec<Record>>,
Json(records): Json<Vec<Record<EncryptedData>>>,
) -> Result<(), ErrorResponseStatus<'static>> {
let State(AppState { database, settings }) = state;
@ -26,7 +26,7 @@ pub async fn post<DB: Database>(
let too_big = records
.iter()
.any(|r| r.data.len() >= settings.max_record_size || settings.max_record_size == 0);
.any(|r| r.data.data.len() >= settings.max_record_size || settings.max_record_size == 0);
if too_big {
return Err(
@ -84,7 +84,7 @@ pub async fn next<DB: Database>(
params: Query<NextParams>,
UserAuth(user): UserAuth,
state: State<AppState<DB>>,
) -> Result<Json<Vec<Record>>, ErrorResponseStatus<'static>> {
) -> Result<Json<Vec<Record<EncryptedData>>>, ErrorResponseStatus<'static>> {
let State(AppState { database, settings }) = state;
let params = params.0;