Skip to main content

fedimint_server/config/
io.rs

1use 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
16/// Client configuration file
17pub const CLIENT_CONFIG: &str = "client";
18
19/// Server private keys file
20pub const PRIVATE_CONFIG: &str = "private";
21
22/// Server locally configurable file
23pub const LOCAL_CONFIG: &str = "local";
24
25/// Server consensus-only configurable file
26pub const CONSENSUS_CONFIG: &str = "consensus";
27
28/// Client connection string file
29pub const CLIENT_INVITE_CODE_FILE: &str = "invite-code";
30
31/// Legacy salt file for combining with the private key
32pub(crate) const SALT_FILE: &str = "private.salt";
33
34/// Legacy plain-text stored password
35pub const PLAINTEXT_PASSWORD: &str = "password.private";
36
37/// Legacy encrypted file extension
38pub(crate) const ENCRYPTED_EXT: &str = "encrypt";
39
40/// Database file name
41pub const DB_FILE: &str = "database";
42
43pub const JSON_EXT: &str = "json";
44
45/// Reads the server config from plaintext JSON files. Falls back to reading
46/// the legacy encrypted format using the password.private file if the
47/// plaintext private config does not exist.
48pub 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
56/// Reads the server config from plaintext JSON files
57fn 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
65/// Reads the server config with the legacy encrypted private config format,
66/// using the password from the password.private file.
67fn 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
79/// Parses the current plaintext guardian backup format from raw file contents.
80pub(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
92/// Parses the legacy encrypted guardian backup format from raw file contents,
93/// decrypting the private config with the provided password. The `salt` and
94/// `private` bytes come from the backup's `private.salt` and `private.encrypt`
95/// entries.
96pub(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
118/// Reads a plaintext json file into a struct
119fn 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
124/// Reads an encrypted json file into a struct
125fn 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
135/// Writes the server into configuration files as plaintext JSON.
136pub 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
160/// Writes struct into a plaintext json file
161fn 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
184/// We definitely don't want leading/trailing newlines in passwords, and a user
185/// editing the file manually will probably get a free newline added
186/// by the text editor.
187fn 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}