server: Add a check for a changing private key

This checks that the private key used to encode the passwords has not
changed since last successful startup, leading to a corruption of all
the passwords. Lots of common scenario are covered, with various
combinations of key in a file or from a seed, set in the config file or
in an env variable or through CLI, and so on.
This commit is contained in:
Valentin Tolmer
2023-12-29 08:28:48 +01:00
committed by nitnelave
parent 997119cdcf
commit f2b1e73929
7 changed files with 524 additions and 33 deletions

View File

@@ -5,7 +5,8 @@ use crate::domain::{
use itertools::Itertools;
use sea_orm::{
sea_query::{
self, all, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Func, Index, Query, Table, Value,
self, all, BlobSize::Blob, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Func, Index,
Query, Table, Value,
},
ConnectionTrait, DatabaseTransaction, DbErr, DeriveIden, FromQueryResult, Iden, Order,
Statement, TransactionTrait,
@@ -91,6 +92,8 @@ pub enum Metadata {
Table,
// Which version of the schema we're at.
Version,
PrivateKeyHash,
PrivateKeyLocation,
}
#[derive(FromQueryResult, PartialEq, Eq, Debug)]
@@ -924,6 +927,28 @@ async fn migrate_to_v6(transaction: DatabaseTransaction) -> Result<DatabaseTrans
Ok(transaction)
}
async fn migrate_to_v7(transaction: DatabaseTransaction) -> Result<DatabaseTransaction, DbErr> {
let builder = transaction.get_database_backend();
transaction
.execute(
builder.build(
Table::alter()
.table(Metadata::Table)
.add_column(ColumnDef::new(Metadata::PrivateKeyHash).blob(Blob(Some(32)))),
),
)
.await?;
transaction
.execute(
builder.build(
Table::alter()
.table(Metadata::Table)
.add_column(ColumnDef::new(Metadata::PrivateKeyLocation).string_len(255)),
),
)
.await?;
Ok(transaction)
}
// This is needed to make an array of async functions.
macro_rules! to_sync {
($l:ident) => {
@@ -950,6 +975,7 @@ pub async fn migrate_from_version(
to_sync!(migrate_to_v4),
to_sync!(migrate_to_v5),
to_sync!(migrate_to_v6),
to_sync!(migrate_to_v7),
];
assert_eq!(migrations.len(), (LAST_SCHEMA_VERSION.0 - 1) as usize);
for migration in 2..=last_version.0 {

View File

@@ -1,12 +1,47 @@
use super::sql_migrations::{get_schema_version, migrate_from_version, upgrade_to_v1};
use sea_orm::{DeriveValueType, QueryResult, Value};
use crate::domain::sql_migrations::{
get_schema_version, migrate_from_version, upgrade_to_v1, Metadata,
};
use sea_orm::{
sea_query::Query, ConnectionTrait, DeriveValueType, Iden, QueryResult, TryGetable, Value,
};
use serde::{Deserialize, Serialize};
pub type DbConnection = sea_orm::DatabaseConnection;
#[derive(Copy, PartialEq, Eq, Debug, Clone, PartialOrd, Ord, DeriveValueType)]
pub struct SchemaVersion(pub i16);
pub const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(6);
pub const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(7);
#[derive(Copy, PartialEq, Eq, Debug, Clone, PartialOrd, Ord)]
pub struct PrivateKeyHash(pub [u8; 32]);
impl TryGetable for PrivateKeyHash {
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, sea_orm::TryGetError> {
let index = format!("{pre}{col}");
Self::try_get_by(res, index.as_str())
}
fn try_get_by_index(res: &QueryResult, index: usize) -> Result<Self, sea_orm::TryGetError> {
Self::try_get_by(res, index)
}
fn try_get_by<I: sea_orm::ColIdx>(
res: &QueryResult,
index: I,
) -> Result<Self, sea_orm::TryGetError> {
Ok(PrivateKeyHash(
std::convert::TryInto::<[u8; 32]>::try_into(res.try_get_by::<Vec<u8>, I>(index)?)
.unwrap(),
))
}
}
impl From<PrivateKeyHash> for Value {
fn from(val: PrivateKeyHash) -> Self {
Self::from(val.0.to_vec())
}
}
pub async fn init_table(pool: &DbConnection) -> anyhow::Result<()> {
let version = {
@@ -21,6 +56,71 @@ pub async fn init_table(pool: &DbConnection) -> anyhow::Result<()> {
Ok(())
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum ConfigLocation {
ConfigFile(String),
EnvironmentVariable(String),
CommandLine,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub enum PrivateKeyLocation {
KeySeed(ConfigLocation),
KeyFile(ConfigLocation, std::ffi::OsString),
Default,
#[cfg(test)]
Tests,
}
#[derive(Debug)]
pub struct PrivateKeyInfo {
pub private_key_hash: PrivateKeyHash,
pub private_key_location: PrivateKeyLocation,
}
pub async fn get_private_key_info(pool: &DbConnection) -> anyhow::Result<Option<PrivateKeyInfo>> {
let result = pool
.query_one(
pool.get_database_backend().build(
Query::select()
.column(Metadata::PrivateKeyHash)
.column(Metadata::PrivateKeyLocation)
.from(Metadata::Table),
),
)
.await?;
let result = match result {
None => return Ok(None),
Some(r) => r,
};
if let Ok(hash) = result.try_get("", &Metadata::PrivateKeyHash.to_string()) {
Ok(Some(PrivateKeyInfo {
private_key_hash: hash,
private_key_location: serde_json::from_str(
&result.try_get::<String>("", &Metadata::PrivateKeyLocation.to_string())?,
)?,
}))
} else {
Ok(None)
}
}
pub async fn set_private_key_info(pool: &DbConnection, info: PrivateKeyInfo) -> anyhow::Result<()> {
pool.execute(
pool.get_database_backend().build(
Query::update()
.table(Metadata::Table)
.value(Metadata::PrivateKeyHash, Value::from(info.private_key_hash))
.value(
Metadata::PrivateKeyLocation,
Value::from(serde_json::to_string(&info.private_key_location).unwrap()),
),
),
)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use crate::domain::{