fedimint_server/config/
io.rs1use std::fmt::Display;
2use std::fs;
3use std::path::Path;
4
5use anyhow::Context as _;
6use fedimint_aead::{LessSafeKey, decrypt, encrypted_read, get_encryption_key};
7use fedimint_core::invite_code::InviteCode;
8use fedimint_logging::LOG_CORE;
9use fedimint_server_core::ServerModuleInitRegistry;
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12use tracing::warn;
13
14use crate::config::ServerConfig;
15
16pub const CLIENT_CONFIG: &str = "client";
18
19pub const PRIVATE_CONFIG: &str = "private";
21
22pub const LOCAL_CONFIG: &str = "local";
24
25pub const CONSENSUS_CONFIG: &str = "consensus";
27
28pub const CLIENT_INVITE_CODE_FILE: &str = "invite-code";
30
31pub(crate) const SALT_FILE: &str = "private.salt";
33
34pub const PLAINTEXT_PASSWORD: &str = "password.private";
36
37pub(crate) const ENCRYPTED_EXT: &str = "encrypt";
39
40pub const DB_FILE: &str = "database";
42
43pub const JSON_EXT: &str = "json";
44
45pub fn read_server_config(path: &Path) -> anyhow::Result<ServerConfig> {
49 if path.join(PRIVATE_CONFIG).with_extension(JSON_EXT).exists() {
50 return read_server_config_plaintext(path);
51 }
52
53 read_server_config_legacy_encrypted(path)
54}
55
56fn read_server_config_plaintext(path: &Path) -> anyhow::Result<ServerConfig> {
58 Ok(ServerConfig {
59 consensus: plaintext_json_read(&path.join(CONSENSUS_CONFIG))?,
60 local: plaintext_json_read(&path.join(LOCAL_CONFIG))?,
61 private: plaintext_json_read(&path.join(PRIVATE_CONFIG))?,
62 })
63}
64
65fn read_server_config_legacy_encrypted(path: &Path) -> anyhow::Result<ServerConfig> {
68 let password = fs::read_to_string(path.join(PLAINTEXT_PASSWORD))?;
69 let salt = fs::read_to_string(path.join(SALT_FILE))?;
70 let key = get_encryption_key(trim_password(&password), &salt)?;
71
72 Ok(ServerConfig {
73 consensus: plaintext_json_read(&path.join(CONSENSUS_CONFIG))?,
74 local: plaintext_json_read(&path.join(LOCAL_CONFIG))?,
75 private: encrypted_json_read(&key, &path.join(PRIVATE_CONFIG))?,
76 })
77}
78
79pub(crate) fn parse_plaintext_backup(
81 local: &[u8],
82 consensus: &[u8],
83 private: &[u8],
84) -> anyhow::Result<ServerConfig> {
85 Ok(ServerConfig {
86 consensus: serde_json::from_slice(consensus)?,
87 local: serde_json::from_slice(local)?,
88 private: serde_json::from_slice(private)?,
89 })
90}
91
92pub(crate) fn parse_legacy_encrypted_backup(
97 local: &[u8],
98 consensus: &[u8],
99 private: &[u8],
100 salt: &[u8],
101 password: &str,
102) -> anyhow::Result<ServerConfig> {
103 let salt = std::str::from_utf8(salt).context("Salt is not valid UTF-8")?;
104 let key = get_encryption_key(trim_password(password), salt)?;
105
106 let mut ciphertext =
107 hex::decode(private).context("Encrypted private config is not valid hex")?;
108 let decrypted = decrypt(&mut ciphertext, &key)
109 .context("Failed to decrypt the private config, the password may be incorrect")?;
110
111 Ok(ServerConfig {
112 consensus: serde_json::from_slice(consensus)?,
113 local: serde_json::from_slice(local)?,
114 private: serde_json::from_slice(decrypted)?,
115 })
116}
117
118fn plaintext_json_read<T: Serialize + DeserializeOwned>(path: &Path) -> anyhow::Result<T> {
120 let string = fs::read_to_string(path.with_extension(JSON_EXT))?;
121 Ok(serde_json::from_str(&string)?)
122}
123
124fn encrypted_json_read<T: Serialize + DeserializeOwned>(
126 key: &LessSafeKey,
127 path: &Path,
128) -> anyhow::Result<T> {
129 let decrypted = encrypted_read(key, path.with_extension(ENCRYPTED_EXT))
130 .context("Failed to decrypt the private config, the password may be incorrect")?;
131 let string = String::from_utf8(decrypted)?;
132 Ok(serde_json::from_str(&string)?)
133}
134
135pub fn write_server_config(
137 server: &ServerConfig,
138 path: &Path,
139 module_config_gens: &ServerModuleInitRegistry,
140 api_secret: Option<String>,
141) -> anyhow::Result<()> {
142 let client_config = server.consensus.to_client_config(module_config_gens)?;
143 plaintext_json_write(&server.local, &path.join(LOCAL_CONFIG))?;
144 plaintext_json_write(&server.consensus, &path.join(CONSENSUS_CONFIG))?;
145 plaintext_display_write(
146 &InviteCode::new(
147 server.consensus.api_endpoints()[&server.local.identity]
148 .url
149 .clone(),
150 server.local.identity,
151 server.calculate_federation_id(),
152 api_secret,
153 ),
154 &path.join(CLIENT_INVITE_CODE_FILE),
155 )?;
156 plaintext_json_write(&client_config, &path.join(CLIENT_CONFIG))?;
157 plaintext_json_write(&server.private, &path.join(PRIVATE_CONFIG))
158}
159
160fn plaintext_json_write<T: Serialize + DeserializeOwned>(
162 obj: &T,
163 path: &Path,
164) -> anyhow::Result<()> {
165 let file = fs::File::options()
166 .create_new(true)
167 .write(true)
168 .open(path.with_extension(JSON_EXT))?;
169
170 serde_json::to_writer_pretty(file, obj)?;
171 Ok(())
172}
173
174fn plaintext_display_write<T: Display>(obj: &T, path: &Path) -> anyhow::Result<()> {
175 use std::io::Write;
176 let mut file = fs::File::options()
177 .create_new(true)
178 .write(true)
179 .open(path)?;
180 file.write_all(obj.to_string().as_bytes())?;
181 Ok(())
182}
183
184fn trim_password(password: &str) -> &str {
188 let password_fully_trimmed = password.trim();
189 if password_fully_trimmed != password {
190 warn!(
191 target: LOG_CORE,
192 "Password in the password file contains leading/trailing whitespaces. This will an error in the future."
193 );
194 }
195 password_fully_trimmed
196}