fedimint_server/config/
io.rs1use std::fmt::Display;
2use std::fs;
3use std::fs::OpenOptions;
4use std::io::Write;
5use std::path::{Path, PathBuf};
6
7use anyhow::ensure;
8use fedimint_aead::{LessSafeKey, encrypted_read, encrypted_write, get_encryption_key};
9use fedimint_core::module::ApiAuth;
10use fedimint_core::util::write_new;
11use fedimint_logging::LOG_CORE;
12use fedimint_server_core::ServerModuleInitRegistry;
13use serde::Serialize;
14use serde::de::DeserializeOwned;
15use tracing::{debug, info, warn};
16
17use crate::config::{ServerConfig, ServerConfigPrivate};
18
19pub const CLIENT_CONFIG: &str = "client";
21
22pub const PRIVATE_CONFIG: &str = "private";
24
25pub const LOCAL_CONFIG: &str = "local";
27
28pub const CONSENSUS_CONFIG: &str = "consensus";
30
31pub const CLIENT_INVITE_CODE_FILE: &str = "invite-code";
33
34pub const SALT_FILE: &str = "private.salt";
36
37pub const PLAINTEXT_PASSWORD: &str = "password.private";
40
41pub const DB_FILE: &str = "database";
43
44pub const JSON_EXT: &str = "json";
45
46pub const ENCRYPTED_EXT: &str = "encrypt";
47
48pub const NEW_VERSION_FILE_EXT: &str = "new";
49
50pub fn read_server_config(password: &str, path: &Path) -> anyhow::Result<ServerConfig> {
52 let salt = fs::read_to_string(path.join(SALT_FILE))?;
53 let key = get_encryption_key(password, &salt)?;
54
55 Ok(ServerConfig {
56 consensus: plaintext_json_read(&path.join(CONSENSUS_CONFIG))?,
57 local: plaintext_json_read(&path.join(LOCAL_CONFIG))?,
58 private: encrypted_json_read(&key, &path.join(PRIVATE_CONFIG))?,
59 })
60}
61
62fn plaintext_json_read<T: Serialize + DeserializeOwned>(path: &Path) -> anyhow::Result<T> {
64 let string = fs::read_to_string(path.with_extension(JSON_EXT))?;
65 Ok(serde_json::from_str(&string)?)
66}
67
68fn encrypted_json_read<T: Serialize + DeserializeOwned>(
70 key: &LessSafeKey,
71 path: &Path,
72) -> anyhow::Result<T> {
73 let decrypted = encrypted_read(key, path.with_extension(ENCRYPTED_EXT));
74 let string = String::from_utf8(decrypted?)?;
75 Ok(serde_json::from_str(&string)?)
76}
77
78pub fn write_server_config(
80 server: &ServerConfig,
81 path: &Path,
82 password: &str,
83 module_config_gens: &ServerModuleInitRegistry,
84 api_secret: Option<String>,
85) -> anyhow::Result<()> {
86 let salt = fs::read_to_string(path.join(SALT_FILE))?;
87 let key = get_encryption_key(password, &salt)?;
88
89 let client_config = server.consensus.to_client_config(module_config_gens)?;
90 plaintext_json_write(&server.local, &path.join(LOCAL_CONFIG))?;
91 plaintext_json_write(&server.consensus, &path.join(CONSENSUS_CONFIG))?;
92 plaintext_display_write(
93 &server.get_invite_code(api_secret),
94 &path.join(CLIENT_INVITE_CODE_FILE),
95 )?;
96 plaintext_json_write(&client_config, &path.join(CLIENT_CONFIG))?;
97 encrypted_json_write(&server.private, &key, &path.join(PRIVATE_CONFIG))
98}
99
100fn plaintext_json_write<T: Serialize + DeserializeOwned>(
102 obj: &T,
103 path: &Path,
104) -> anyhow::Result<()> {
105 let file = fs::File::options()
106 .create_new(true)
107 .write(true)
108 .open(path.with_extension(JSON_EXT))?;
109
110 serde_json::to_writer_pretty(file, obj)?;
111 Ok(())
112}
113
114fn plaintext_display_write<T: Display>(obj: &T, path: &Path) -> anyhow::Result<()> {
115 let mut file = fs::File::options()
116 .create_new(true)
117 .write(true)
118 .open(path)?;
119 file.write_all(obj.to_string().as_bytes())?;
120 Ok(())
121}
122
123pub fn encrypted_json_write<T: Serialize + DeserializeOwned>(
125 obj: &T,
126 key: &LessSafeKey,
127 path: &Path,
128) -> anyhow::Result<()> {
129 let bytes = serde_json::to_string(obj)?.into_bytes();
130 encrypted_write(bytes, key, path.with_extension(ENCRYPTED_EXT))
131}
132
133pub fn trim_password(password: &str) -> &str {
137 let password_fully_trimmed = password.trim();
138 if password_fully_trimmed != password {
139 warn!(
140 target: LOG_CORE,
141 "Password in the password file contains leading/trailing whitespaces. This will an error in the future."
142 );
143 }
144 password_fully_trimmed
145}
146
147pub fn backup_copy_path(original: &Path) -> PathBuf {
148 original.with_extension("bak")
149}
150
151pub fn create_backup_copy(original: &Path) -> anyhow::Result<()> {
152 let backup_path = backup_copy_path(original);
153 info!(target: LOG_CORE, ?original, ?backup_path, "Creating backup copy of file");
154 ensure!(
155 !backup_path.exists(),
156 "Already have a backup at {backup_path:?}, would be overwritten"
157 );
158 fs::copy(original, backup_path)?;
159 Ok(())
160}
161
162pub fn reencrypt_private_config(
173 data_dir: &Path,
174 private_config: &ServerConfigPrivate,
175 new_password: &str,
176) -> anyhow::Result<()> {
177 info!(target: LOG_CORE, ?data_dir, "Re-encrypting private config with new password");
178 let trimmed_password = trim_password(new_password);
179
180 let salt = fs::read_to_string(data_dir.join(SALT_FILE))?;
183 let new_key = get_encryption_key(trimmed_password, &salt)?;
184
185 let password_file_path = data_dir.join(PLAINTEXT_PASSWORD);
186 let private_config_path = data_dir.join(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT);
187
188 debug!(target: LOG_CORE, "Creating backup of private config");
190 let password_file_present = password_file_path.exists();
191 if password_file_present {
192 create_backup_copy(&password_file_path)?;
193 }
194 create_backup_copy(&private_config_path)?;
195
196 OpenOptions::new().read(true).open(data_dir)?.sync_all()?;
198
199 let new_private_config = {
201 let mut new_private_config = private_config.clone();
202 new_private_config.api_auth = ApiAuth::new(trimmed_password.to_string());
203 new_private_config
204 };
205
206 debug!(target: LOG_CORE, "Creating temporary files");
218 let temp_password_file_path = password_file_path.with_extension(NEW_VERSION_FILE_EXT);
219 if password_file_present {
220 write_new(&temp_password_file_path, trimmed_password)?;
221 }
222
223 let temp_private_config_path = private_config_path.with_extension(NEW_VERSION_FILE_EXT);
224 let private_config_bytes = serde_json::to_string(&new_private_config)?.into_bytes();
227 encrypted_write(
228 private_config_bytes,
229 &new_key,
230 temp_private_config_path.clone(),
231 )?;
232
233 OpenOptions::new().read(true).open(data_dir)?.sync_all()?;
235
236 debug!(target: LOG_CORE, "Moving temp files to final location");
237 fs::rename(&temp_private_config_path, &private_config_path)?;
241 if password_file_present {
242 fs::rename(&temp_password_file_path, &password_file_path)?;
243 }
244
245 Ok(())
246}
247
248pub fn recover_interrupted_password_change(data_dir: &Path) -> anyhow::Result<()> {
252 let password_file_path = data_dir.join(PLAINTEXT_PASSWORD);
253 let private_config_path = data_dir.join(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT);
254
255 let temp_password_file_path = password_file_path.with_extension(NEW_VERSION_FILE_EXT);
256 let temp_private_config_path = private_config_path.with_extension(NEW_VERSION_FILE_EXT);
257
258 match (
259 temp_private_config_path.exists(),
260 temp_password_file_path.exists(),
261 ) {
262 (false, false) => {
263 }
265 (true, password_file_exists) => {
266 warn!(
267 target: LOG_CORE,
268 "Found temporary private config, password change process was interrupted. Recovering..."
269 );
270
271 if password_file_exists {
273 fs::rename(&temp_password_file_path, &password_file_path)?;
274 }
275 fs::rename(&temp_private_config_path, &private_config_path)?;
276 }
277 (false, true) => {
278 warn!(
279 target: LOG_CORE,
280 "Found only the temporary password file but no encrypted config. Cleaning up the temporary password file."
281 );
282 fs::remove_file(&temp_password_file_path)?;
283 }
284 }
285
286 Ok(())
287}
288
289pub fn finalize_password_change(data_dir: &Path) -> anyhow::Result<()> {
292 let password_backup_path = backup_copy_path(&data_dir.join(PLAINTEXT_PASSWORD));
293 if password_backup_path.exists() {
294 fs::remove_file(&password_backup_path)?;
295 }
296
297 let private_config_backup_path =
298 backup_copy_path(&data_dir.join(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT));
299 if private_config_backup_path.exists() {
300 fs::remove_file(&private_config_backup_path)?;
301 }
302
303 Ok(())
304}