use std::fmt::Display;
use std::fs;
use std::path::Path;
use anyhow::Context as _;
use fedimint_aead::{LessSafeKey, decrypt, encrypted_read, get_encryption_key};
use fedimint_core::invite_code::InviteCode;
use fedimint_logging::LOG_CORE;
use fedimint_server_core::ServerModuleInitRegistry;
use serde::Serialize;
use serde::de::DeserializeOwned;
use tracing::warn;
use crate::config::ServerConfig;
pub const CLIENT_CONFIG: &str = "client";
pub const PRIVATE_CONFIG: &str = "private";
pub const LOCAL_CONFIG: &str = "local";
pub const CONSENSUS_CONFIG: &str = "consensus";
pub const CLIENT_INVITE_CODE_FILE: &str = "invite-code";
pub(crate) const SALT_FILE: &str = "private.salt";
pub const PLAINTEXT_PASSWORD: &str = "password.private";
pub(crate) const ENCRYPTED_EXT: &str = "encrypt";
pub const DB_FILE: &str = "database";
pub const JSON_EXT: &str = "json";
pub fn read_server_config(path: &Path) -> anyhow::Result<ServerConfig> {
if path.join(PRIVATE_CONFIG).with_extension(JSON_EXT).exists() {
return read_server_config_plaintext(path);
}
read_server_config_legacy_encrypted(path)
}
fn read_server_config_plaintext(path: &Path) -> anyhow::Result<ServerConfig> {
Ok(ServerConfig {
consensus: plaintext_json_read(&path.join(CONSENSUS_CONFIG))?,
local: plaintext_json_read(&path.join(LOCAL_CONFIG))?,
private: plaintext_json_read(&path.join(PRIVATE_CONFIG))?,
})
}
fn read_server_config_legacy_encrypted(path: &Path) -> anyhow::Result<ServerConfig> {
let password = fs::read_to_string(path.join(PLAINTEXT_PASSWORD))?;
let salt = fs::read_to_string(path.join(SALT_FILE))?;
let key = get_encryption_key(trim_password(&password), &salt)?;
Ok(ServerConfig {
consensus: plaintext_json_read(&path.join(CONSENSUS_CONFIG))?,
local: plaintext_json_read(&path.join(LOCAL_CONFIG))?,
private: encrypted_json_read(&key, &path.join(PRIVATE_CONFIG))?,
})
}
pub(crate) fn parse_plaintext_backup(
local: &[u8],
consensus: &[u8],
private: &[u8],
) -> anyhow::Result<ServerConfig> {
Ok(ServerConfig {
consensus: serde_json::from_slice(consensus)?,
local: serde_json::from_slice(local)?,
private: serde_json::from_slice(private)?,
})
}
pub(crate) fn parse_legacy_encrypted_backup(
local: &[u8],
consensus: &[u8],
private: &[u8],
salt: &[u8],
password: &str,
) -> anyhow::Result<ServerConfig> {
let salt = std::str::from_utf8(salt).context("Salt is not valid UTF-8")?;
let key = get_encryption_key(trim_password(password), salt)?;
let mut ciphertext =
hex::decode(private).context("Encrypted private config is not valid hex")?;
let decrypted = decrypt(&mut ciphertext, &key)
.context("Failed to decrypt the private config, the password may be incorrect")?;
Ok(ServerConfig {
consensus: serde_json::from_slice(consensus)?,
local: serde_json::from_slice(local)?,
private: serde_json::from_slice(decrypted)?,
})
}
fn plaintext_json_read<T: Serialize + DeserializeOwned>(path: &Path) -> anyhow::Result<T> {
let string = fs::read_to_string(path.with_extension(JSON_EXT))?;
Ok(serde_json::from_str(&string)?)
}
fn encrypted_json_read<T: Serialize + DeserializeOwned>(
key: &LessSafeKey,
path: &Path,
) -> anyhow::Result<T> {
let decrypted = encrypted_read(key, path.with_extension(ENCRYPTED_EXT))
.context("Failed to decrypt the private config, the password may be incorrect")?;
let string = String::from_utf8(decrypted)?;
Ok(serde_json::from_str(&string)?)
}
pub fn write_server_config(
server: &ServerConfig,
path: &Path,
module_config_gens: &ServerModuleInitRegistry,
api_secret: Option<String>,
) -> anyhow::Result<()> {
let client_config = server.consensus.to_client_config(module_config_gens)?;
plaintext_json_write(&server.local, &path.join(LOCAL_CONFIG))?;
plaintext_json_write(&server.consensus, &path.join(CONSENSUS_CONFIG))?;
plaintext_display_write(
&InviteCode::new(
server.consensus.api_endpoints()[&server.local.identity]
.url
.clone(),
server.local.identity,
server.calculate_federation_id(),
api_secret,
),
&path.join(CLIENT_INVITE_CODE_FILE),
)?;
plaintext_json_write(&client_config, &path.join(CLIENT_CONFIG))?;
plaintext_json_write(&server.private, &path.join(PRIVATE_CONFIG))
}
fn plaintext_json_write<T: Serialize + DeserializeOwned>(
obj: &T,
path: &Path,
) -> anyhow::Result<()> {
let file = fs::File::options()
.create_new(true)
.write(true)
.open(path.with_extension(JSON_EXT))?;
serde_json::to_writer_pretty(file, obj)?;
Ok(())
}
fn plaintext_display_write<T: Display>(obj: &T, path: &Path) -> anyhow::Result<()> {
use std::io::Write;
let mut file = fs::File::options()
.create_new(true)
.write(true)
.open(path)?;
file.write_all(obj.to_string().as_bytes())?;
Ok(())
}
fn trim_password(password: &str) -> &str {
let password_fully_trimmed = password.trim();
if password_fully_trimmed != password {
warn!(
target: LOG_CORE,
"Password in the password file contains leading/trailing whitespaces. This will an error in the future."
);
}
password_fully_trimmed
}