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