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::invite_code::InviteCode;
10use fedimint_core::module::ApiAuth;
11use fedimint_core::util::write_new;
12use fedimint_logging::LOG_CORE;
13use fedimint_server_core::ServerModuleInitRegistry;
14use serde::Serialize;
15use serde::de::DeserializeOwned;
16use tracing::{debug, info, warn};
17
18use crate::config::{ServerConfig, ServerConfigPrivate};
19
20pub const CLIENT_CONFIG: &str = "client";
22
23pub const PRIVATE_CONFIG: &str = "private";
25
26pub const LOCAL_CONFIG: &str = "local";
28
29pub const CONSENSUS_CONFIG: &str = "consensus";
31
32pub const CLIENT_INVITE_CODE_FILE: &str = "invite-code";
34
35pub const SALT_FILE: &str = "private.salt";
37
38pub const PLAINTEXT_PASSWORD: &str = "password.private";
41
42pub const DB_FILE: &str = "database";
44
45pub const JSON_EXT: &str = "json";
46
47pub const ENCRYPTED_EXT: &str = "encrypt";
48
49pub const NEW_VERSION_FILE_EXT: &str = "new";
50
51pub fn read_server_config(password: &str, path: &Path) -> anyhow::Result<ServerConfig> {
53 let salt = fs::read_to_string(path.join(SALT_FILE))?;
54 let key = get_encryption_key(password, &salt)?;
55
56 Ok(ServerConfig {
57 consensus: plaintext_json_read(&path.join(CONSENSUS_CONFIG))?,
58 local: plaintext_json_read(&path.join(LOCAL_CONFIG))?,
59 private: encrypted_json_read(&key, &path.join(PRIVATE_CONFIG))?,
60 })
61}
62
63fn plaintext_json_read<T: Serialize + DeserializeOwned>(path: &Path) -> anyhow::Result<T> {
65 let string = fs::read_to_string(path.with_extension(JSON_EXT))?;
66 Ok(serde_json::from_str(&string)?)
67}
68
69fn encrypted_json_read<T: Serialize + DeserializeOwned>(
71 key: &LessSafeKey,
72 path: &Path,
73) -> anyhow::Result<T> {
74 let decrypted = encrypted_read(key, path.with_extension(ENCRYPTED_EXT));
75 let string = String::from_utf8(decrypted?)?;
76 Ok(serde_json::from_str(&string)?)
77}
78
79pub fn write_server_config(
81 server: &ServerConfig,
82 path: &Path,
83 password: &str,
84 module_config_gens: &ServerModuleInitRegistry,
85 api_secret: Option<String>,
86) -> anyhow::Result<()> {
87 let salt = fs::read_to_string(path.join(SALT_FILE))?;
88 let key = get_encryption_key(password, &salt)?;
89
90 let client_config = server.consensus.to_client_config(module_config_gens)?;
91 plaintext_json_write(&server.local, &path.join(LOCAL_CONFIG))?;
92 plaintext_json_write(&server.consensus, &path.join(CONSENSUS_CONFIG))?;
93 plaintext_display_write(
94 &InviteCode::new(
95 server.consensus.api_endpoints()[&server.local.identity]
96 .url
97 .clone(),
98 server.local.identity,
99 server.calculate_federation_id(),
100 api_secret,
101 ),
102 &path.join(CLIENT_INVITE_CODE_FILE),
103 )?;
104 plaintext_json_write(&client_config, &path.join(CLIENT_CONFIG))?;
105 encrypted_json_write(&server.private, &key, &path.join(PRIVATE_CONFIG))
106}
107
108fn plaintext_json_write<T: Serialize + DeserializeOwned>(
110 obj: &T,
111 path: &Path,
112) -> anyhow::Result<()> {
113 let file = fs::File::options()
114 .create_new(true)
115 .write(true)
116 .open(path.with_extension(JSON_EXT))?;
117
118 serde_json::to_writer_pretty(file, obj)?;
119 Ok(())
120}
121
122fn plaintext_display_write<T: Display>(obj: &T, path: &Path) -> anyhow::Result<()> {
123 let mut file = fs::File::options()
124 .create_new(true)
125 .write(true)
126 .open(path)?;
127 file.write_all(obj.to_string().as_bytes())?;
128 Ok(())
129}
130
131pub fn encrypted_json_write<T: Serialize + DeserializeOwned>(
133 obj: &T,
134 key: &LessSafeKey,
135 path: &Path,
136) -> anyhow::Result<()> {
137 let bytes = serde_json::to_string(obj)?.into_bytes();
138 encrypted_write(bytes, key, path.with_extension(ENCRYPTED_EXT))
139}
140
141pub fn trim_password(password: &str) -> &str {
145 let password_fully_trimmed = password.trim();
146 if password_fully_trimmed != password {
147 warn!(
148 target: LOG_CORE,
149 "Password in the password file contains leading/trailing whitespaces. This will an error in the future."
150 );
151 }
152 password_fully_trimmed
153}
154
155pub fn backup_copy_path(original: &Path) -> PathBuf {
156 original.with_extension("bak")
157}
158
159pub fn create_backup_copy(original: &Path) -> anyhow::Result<()> {
160 let backup_path = backup_copy_path(original);
161 info!(target: LOG_CORE, ?original, ?backup_path, "Creating backup copy of file");
162 ensure!(
163 !backup_path.exists(),
164 "Already have a backup at {}, would be overwritten",
165 backup_path.display()
166 );
167 fs::copy(original, backup_path)?;
168 Ok(())
169}
170
171pub fn reencrypt_private_config(
182 data_dir: &Path,
183 private_config: &ServerConfigPrivate,
184 new_password: &str,
185) -> anyhow::Result<()> {
186 info!(target: LOG_CORE, ?data_dir, "Re-encrypting private config with new password");
187 let trimmed_password = trim_password(new_password);
188
189 let salt = fs::read_to_string(data_dir.join(SALT_FILE))?;
192 let new_key = get_encryption_key(trimmed_password, &salt)?;
193
194 let password_file_path = data_dir.join(PLAINTEXT_PASSWORD);
195 let private_config_path = data_dir.join(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT);
196
197 debug!(target: LOG_CORE, "Creating backup of private config");
199 let password_file_present = password_file_path.exists();
200 if password_file_present {
201 create_backup_copy(&password_file_path)?;
202 }
203 create_backup_copy(&private_config_path)?;
204
205 OpenOptions::new().read(true).open(data_dir)?.sync_all()?;
207
208 let new_private_config = {
210 let mut new_private_config = private_config.clone();
211 new_private_config.api_auth = ApiAuth::new(trimmed_password.to_string());
212 new_private_config
213 };
214
215 debug!(target: LOG_CORE, "Creating temporary files");
227 let temp_password_file_path = password_file_path.with_extension(NEW_VERSION_FILE_EXT);
228 if password_file_present {
229 write_new(&temp_password_file_path, trimmed_password)?;
230 }
231
232 let temp_private_config_path = private_config_path.with_extension(NEW_VERSION_FILE_EXT);
233 let private_config_bytes = serde_json::to_string(&new_private_config)?.into_bytes();
236 encrypted_write(
237 private_config_bytes,
238 &new_key,
239 temp_private_config_path.clone(),
240 )?;
241
242 OpenOptions::new().read(true).open(data_dir)?.sync_all()?;
244
245 debug!(target: LOG_CORE, "Moving temp files to final location");
246 fs::rename(&temp_private_config_path, &private_config_path)?;
250 if password_file_present {
251 fs::rename(&temp_password_file_path, &password_file_path)?;
252 }
253
254 Ok(())
255}
256
257pub fn recover_interrupted_password_change(data_dir: &Path) -> anyhow::Result<()> {
261 let password_file_path = data_dir.join(PLAINTEXT_PASSWORD);
262 let private_config_path = data_dir.join(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT);
263
264 let temp_password_file_path = password_file_path.with_extension(NEW_VERSION_FILE_EXT);
265 let temp_private_config_path = private_config_path.with_extension(NEW_VERSION_FILE_EXT);
266
267 match (
268 temp_private_config_path.exists(),
269 temp_password_file_path.exists(),
270 ) {
271 (false, false) => {
272 }
274 (true, password_file_exists) => {
275 warn!(
276 target: LOG_CORE,
277 "Found temporary private config, password change process was interrupted. Recovering..."
278 );
279
280 if password_file_exists {
282 fs::rename(&temp_password_file_path, &password_file_path)?;
283 }
284 fs::rename(&temp_private_config_path, &private_config_path)?;
285 }
286 (false, true) => {
287 warn!(
288 target: LOG_CORE,
289 "Found only the temporary password file but no encrypted config. Cleaning up the temporary password file."
290 );
291 fs::remove_file(&temp_password_file_path)?;
292 }
293 }
294
295 Ok(())
296}
297
298pub fn finalize_password_change(data_dir: &Path) -> anyhow::Result<()> {
301 let password_backup_path = backup_copy_path(&data_dir.join(PLAINTEXT_PASSWORD));
302 if password_backup_path.exists() {
303 fs::remove_file(&password_backup_path)?;
304 }
305
306 let private_config_backup_path =
307 backup_copy_path(&data_dir.join(PRIVATE_CONFIG).with_extension(ENCRYPTED_EXT));
308 if private_config_backup_path.exists() {
309 fs::remove_file(&private_config_backup_path)?;
310 }
311
312 Ok(())
313}