Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/src/app/repl.rs

36.9 KiB, 206 runs

created by r1870400018:953, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use crate::{
2 app::{
3 cfg::AppConfig,
4 constant as app_const,
5 ext::AppExtension,
6 tui::AppStatus,
7 },
8 srv::{
9 admin::audit,
10 api::ApiHandlerRegistry,
11 cert::Certificate,
12 cfg::ServerConfig,
13 constant as srv_const,
14 webhook::WebhookRegistry,
15 },
16};
17
18use std::sync::Arc;
19
20use oxedyne_fe2o3_core::{
21 prelude::*,
22 file as core_file,
23 mem::Extract,
24 path::NormalPath,
25};
26use oxedyne_fe2o3_crypto::{
27 enc::EncryptionScheme,
28 keystore::Wallet,
29};
30use oxedyne_fe2o3_hash::{
31 kdf::KeyDerivationScheme,
32};
33use oxedyne_fe2o3_iop_crypto::{
34 keys::KeyManager,
35 enc::Encrypter,
36};
37use oxedyne_fe2o3_iop_hash::kdf::KeyDeriver;
38use oxedyne_fe2o3_jdat::{
39 prelude::*,
40 file::JdatFile,
41 string::enc::EncoderConfig,
42};
43use oxedyne_fe2o3_syntax::{
44 core::SyntaxRef,
45 help::Help,
46 msg::{
47 Msg,
48 MsgCmd,
49 },
50 opt::OptionRefVec,
51};
52use oxedyne_fe2o3_text::base2x;
53use oxedyne_fe2o3_tui::lib_tui::{
54 cmds,
55 repl::{
56 Evaluation,
57 Shell,
58 ShellConfig,
59 ShellContext,
60 Splitters,
61 },
62 input::UserInput,
63};
64use oxedyne_fe2o3_namex::InNamex;
65
66use std::{
67 collections::BTreeMap,
68 path::{
69 Path,
70 },
71 sync::RwLock,
72};
73
74use secrecy::{
75 ExposeSecret,
76 Secret,
77};
78use zeroize::Zeroize;
79
80
81#[derive(Clone)]
82pub struct AppShellContext {
83 pub stat: AppStatus,
84 pub app_cfg: AppConfig,
85 pub syntax: SyntaxRef,
86 pub ws: BTreeMap<Dat, Dat>,
87 pub db_enc_key: Option<Vec<u8>>,
88 pub wallet: Arc<RwLock<Wallet>>,
89 pub unlocked_admin_name: String,
90 pub unlocked_admin_scopes: Vec<String>,
91 pub webhook_registry: Arc<WebhookRegistry>,
92 pub api_handler_registry: Arc<ApiHandlerRegistry>,
93 pub extension: Arc<dyn AppExtension>,
94}
95
96impl ShellContext for AppShellContext {
97 fn eval(
98 &mut self,
99 input: &String,
100 cfg: &ShellConfig,
101 splitters: &Splitters,
102 )
103 -> Outcome<Vec<Evaluation>>
104 {
105 for expr in splitters.command.split(input).into_iter() {
106 let parts = splitters.assignment.split(expr.val_ref());
107 // 1. try state manipulation
108 match parts.len() {
109 0 => unreachable!(),
110 1 => { // evaluation
111 //let lhs = Dat::decode_string(parts[0].val_ref())?;
112 ////if lhs.kind() != Kind::Str {
113 //// return Err(Error::Local{
114 //// tags: vec![ErrTag::Input, ErrTag::Mismatch],
115 //// kind: ErrKind::Unexpected,
116 //// msg: errmsg!(
117 //// "The left hand side of the assignment is a {:?} but must be a Kind::Str.",
118 //// lhs.kind(),
119 //// )});
120 ////}
121 //if let Some(rhs) = state.get_recursive(&lhs) {
122 // println!("{} = {:?}", lhs, rhs);
123 //} else {
124 // println!("{:?}", lhs);
125 //}
126 //continue;
127 },
128 2 => { // assignment lhs = rhs
129 let lhs = res!(Dat::decode_string(parts[0].val_ref()));
130 let rhs = res!(Dat::decode_string(parts[1].val_ref()));
131 if lhs.kind() != Kind::Str {
132 return Err(err!(
133 "The left hand side of the assignment is a {:?} but must be a Kind::Str.",
134 lhs.kind();
135 Input, Mismatch));
136 }
137 self.ws.insert(lhs, rhs);
138 continue;
139 },
140 _ => return Err(err!(
141 "Only single assignment such as a = b is permitted.";
142 Input, Mismatch)),
143 }
144 // 2. Try syntax command
145 // Split into words and downgrade from phrases to string iterator.
146 let mut parts = splitters.word
147 .split(expr.val_ref())
148 .into_iter()
149 .map(|x| x.to_val())
150 .peekable();
151 // Currently the "echo" command is not in the syntax and therefore not in the help.
152 if let Some("echo") = parts.peek().map(|s| s.as_ref()) {
153 return Ok(vec![Evaluation::Output(input.clone())]);
154 }
155 return self.execute(parts, &cfg);
156 }
157 Ok(vec![Evaluation::None])
158 }
159}
160
161impl AppShellContext {
162
163 pub fn execute<I: IntoIterator<Item=String>>(
164 &mut self,
165 parts: I,
166 shell_cfg: &ShellConfig,
167 )
168 -> Outcome<Vec<Evaluation>>
169 {
170 let mut evals = Vec::new();
171 let msgrx = Msg::new(self.syntax.clone());
172 let msgrx = res!(msgrx.rx_text_iter(
173 parts,
174 Some(app_const::SYNTAX_CMD_SIMILARITY_THRESHOLD),
175 ));
176 for (cmd_key, cmd) in &msgrx.cmds {
177 match cmd_key.as_str() {
178 "help" => {
179 let help = Help::default(); // TODO consider creating only once?
180 for line in res!(help.to_lines(&self.syntax)) {
181 println!("{}", line);
182 }
183 },
184 // Control
185 "exit" => evals.push(res!(cmds::exit_shell(&shell_cfg.exit_msg))),
186 "server" => evals.push(res!(self.start_server(&shell_cfg, Some(cmd)))),
187 "shell" => evals.push(res!(self.start_shell(&shell_cfg, Some(cmd)))),
188 "cert" => evals.push(res!(self.manage_certificates(&shell_cfg, Some(cmd)))),
189 "acme" => evals.push(res!(self.manage_acme(&shell_cfg, Some(cmd)))),
190 // Filesystem
191 "cd" => evals.push(res!(cmds::change_directory(cmd))),
192 "ls" => evals.push(res!(cmds::list_directory_contents(cmd))),
193 "pwd" => evals.push(res!(cmds::print_working_directory())),
194 // Wallet
195 "unseal" => evals.push(res!(self.unseal(&shell_cfg, Some(cmd)))),
196 "secrets" => evals.push(res!(self.secrets(&shell_cfg, Some(cmd)))),
197 "wallet" => evals.push(res!(self.manage_wallet(&shell_cfg, Some(cmd)))),
198 "admin" => evals.push(res!(self.manage_admin(&shell_cfg, Some(cmd)))),
199 // Mail
200 "mailpass" => evals.push(res!(self.mailpass(&shell_cfg, Some(cmd)))),
201 _ => {
202 // Not a built-in command -- offer it to the app
203 // extension. Cloning the Arc is cheap and lets the
204 // borrow checker see that `self` is not aliased.
205 let ext = self.extension.clone();
206 match res!(ext.dispatch_cmd(cmd_key.as_str(), cmd, shell_cfg)) {
207 Some(eval) => evals.push(eval),
208 None => {
209 warn!("Command '{}' is not implemented.", cmd_key);
210 }
211 }
212 }
213 }
214 }
215 Ok(evals)
216 }
217
218 pub fn start_shell(
219 &mut self,
220 shell_cfg: &ShellConfig,
221 _cmd: Option<&MsgCmd>,
222 )
223 -> Outcome<Evaluation>
224 {
225 let mut shell = res!(Shell::new(
226 shell_cfg.clone(),
227 self.clone(),
228 ));
229 res!(shell.start());
230 Ok(Evaluation::None)
231 }
232
233 pub fn secrets(
234 &mut self,
235 _shell_cfg: &ShellConfig,
236 cmd: Option<&MsgCmd>,
237 )
238 -> Outcome<Evaluation>
239 {
240 if let Some(msg_cmd) = cmd {
241 if msg_cmd.has_args() {
242 if res!(msg_cmd.has_only_arg("create")) {
243 let vals = res!(msg_cmd.get_arg_vals("create").with_len(1));
244 let name = &vals[0];
245 let pass = res!(UserInput::ask_for_secret(None));
246 let mut kdf = res!(KeyDerivationScheme::from_str(&self.app_cfg.kdf_name));
247 let key = res!(UserInput::derive_key(&mut kdf, pass));
248 let already_present = {
249 let w = lock_read!(self.wallet);
250 w.enc_secs().get(name).is_some()
251 };
252 if already_present {
253 if res!(UserInput::ask(
254 fmt!("Encrypted secret '{}' already exists, replace? (Y/N): ", name).as_str(),
255 )).to_lowercase().as_str() != "y" {
256 return Ok(Evaluation::Output(fmt!("Creation of encrypted secret aborted.")));
257 }
258 }
259 let mut map = DaticleMap::new();
260 map.insert(dat!("kdf_name"), dat!(fmt!("{}", kdf)));
261 map.insert(dat!("kdf_nid"), dat!(fmt!("{}", res!(kdf.name_id()))));
262 map.insert(dat!("kdf_cfg"), dat!(res!(kdf.encode_cfg_to_string())));
263 let enc = res!(EncryptionScheme::new_aes_256_gcm_with_key(&key));
264 map.insert(dat!("enc_name"), dat!(fmt!("{:?}", enc)));
265 map.insert(dat!("enc_nid"), dat!(fmt!("{}", res!(enc.name_id()))));
266 let sec = res!(UserInput::ask_for_secret(
267 Some("Enter the secret you want to encrypt: ")
268 ));
269 let enc_sec = res!(enc.encrypt(sec.expose_secret().as_bytes()));
270 let base2x = base2x::HEMATITE64;
271 let b2x_sec = base2x.to_string(&enc_sec);
272 map.insert(dat!("enc_sec"), dat!(b2x_sec));
273 let wallet_path = Path::new("./").join(app_const::WALLET_NAME);
274 {
275 let mut w = lock_write!(self.wallet);
276 if let Some(enc_sec_map) = w.enc_secs_mut().get_mut(name) {
277 *enc_sec_map = dat!(map);
278 } else {
279 w.enc_secs_mut().insert(name.clone(), dat!(map));
280 }
281 res!(w.save_secret(
282 &wallet_path, " ", Some(EncoderConfig::<(), ()>::default()),
283 ));
284 }
285 } else if res!(msg_cmd.has_only_arg("recover")) {
286 let vals = res!(msg_cmd.get_arg_vals("recover").with_len(1));
287 let name = &vals[0];
288 // Clone the encrypted-secret map out of the wallet
289 // so we can drop the read lock before the
290 // interactive passphrase prompt below.
291 let enc_sec_dat = {
292 let w = lock_read!(self.wallet);
293 match w.enc_secs().get(name) {
294 Some(map_dat) => map_dat.clone(),
295 None => return Ok(Evaluation::Output(
296 fmt!("Secret '{}' not found in wallet.", name)
297 )),
298 }
299 };
300 let enc_sec_dat = &enc_sec_dat;
301 // Derive the encryption key from the wallet passphrase using the kdf
302 // configuration. Drop the pass as soon as we can.
303 let key = {
304 let pass = res!(UserInput::ask_for_secret(None));
305 let pass = pass.expose_secret();
306
307 let kdf_name = try_extract_dat!(
308 res!(enc_sec_dat.map_get_type_must(&dat!("kdf_name"), &[&Kind::Str])),
309 Str,
310 );
311 let mut kdf = res!(KeyDerivationScheme::from_str(&kdf_name));
312 let kdf_cfg = try_extract_dat!(
313 res!(enc_sec_dat.map_get_type_must(&dat!("kdf_cfg"), &[&Kind::Str])),
314 Str,
315 );
316 res!(kdf.decode_cfg_from_string(&kdf_cfg));
317 res!(kdf.derive(pass.as_bytes()));
318 res!(kdf.get_hash()).to_vec()
319 };
320
321 let enc_name = try_extract_dat!(
322 res!(enc_sec_dat.map_get_type_must(&dat!("enc_name"), &[&Kind::Str])),
323 Str,
324 );
325 let mut enc = res!(EncryptionScheme::from_str(&enc_name));
326 enc = res!(enc.set_secret_key(Some(&key)));
327 let enc_sec_base2x = try_extract_dat!(
328 res!(enc_sec_dat.map_get_type_must(&dat!("enc_sec"), &[&Kind::Str])),
329 Str,
330 );
331 let base2x = base2x::HEMATITE64;
332 let enc_sec_byts = res!(base2x.from_str(&enc_sec_base2x));
333 let sec_byts = res!(enc.decrypt(&enc_sec_byts));
334 let mut sec_str = res!(String::from_utf8(sec_byts));
335 res!(UserInput::show_and_clear(
336 Secret::new(fmt!("Press enter to clear: secret is '{}'", sec_str))
337 ));
338 sec_str.zeroize();
339 }
340 } else {
341 return Err(err!("Missing message command."; Invalid, Input, Missing));
342 }
343 }
344 Ok(Evaluation::None)
345 }
346
347 pub fn mailpass(
348 &mut self,
349 _shell_cfg: &ShellConfig,
350 cmd: Option<&MsgCmd>,
351 )
352 -> Outcome<Evaluation>
353 {
354 let msg_cmd = match cmd {
355 Some(c) => c,
356 None => return Err(err!(
357 "mailpass requires arguments."; Invalid, Input, Missing)),
358 };
359 let address_vals = res!(msg_cmd.get_arg_vals("address").with_len(1));
360 let address = try_extract_dat!(&address_vals[0], Str).clone();
361 let delivery_vals = res!(msg_cmd.get_arg_vals("delivery-dir").with_len(1));
362 let delivery = try_extract_dat!(&delivery_vals[0], Str).clone();
363 // Allow the password to be supplied via the STEEL_MAIL_PASS
364 // env var so this command works in non-interactive contexts
365 // (CI, scripts, deploy automation).
366 let pass: secrecy::Secret<String> = match std::env::var("STEEL_MAIL_PASS") {
367 Ok(p) => secrecy::Secret::new(p),
368 Err(_) => res!(UserInput::ask_for_secret(
369 Some("Enter password for mailbox: "),
370 )),
371 };
372 // Use a moderate cost so the prompt feels snappy. 64 MB / 3
373 // iterations is the OWASP minimum for Argon2id.
374 let mut kdf = res!(KeyDerivationScheme::new_argon2(
375 "Argon2id",
376 0x13,
377 65_536,
378 3,
379 16,
380 32,
381 ));
382 res!(kdf.derive(pass.expose_secret().as_bytes()));
383 let encoded = res!(kdf.encode_to_string());
384 // Print the whole file, not just the entry. The entry alone invites
385 // the reader to paste it into a bare list, which the parser rejects
386 // with "no 'users' list" -- and the only clue that the wrapper exists
387 // is a doc comment in another crate.
388 println!();
389 println!("Add this entry to the \"users\" list in your mail users.jdat.");
390 println!("A file with a single user looks like this in full:");
391 println!();
392 println!(" {{");
393 println!(" \"users\": [");
394 println!(" {{");
395 println!(" \"address\": \"{}\",", address);
396 println!(" \"delivery_dir\": \"{}\",", delivery);
397 println!(" \"argon2id\": \"{}\"", encoded);
398 println!(" }}");
399 println!(" ]");
400 println!(" }}");
401 println!();
402 Ok(Evaluation::None)
403 }
404
405 pub fn manage_wallet(
406 &mut self,
407 _shell_cfg: &ShellConfig,
408 cmd: Option<&MsgCmd>,
409 )
410 -> Outcome<Evaluation>
411 {
412 let msg_cmd = match cmd {
413 Some(c) => c,
414 None => return Err(err!(
415 "wallet requires a subcommand argument."; Invalid, Input, Missing)),
416 };
417 if res!(msg_cmd.has_only_arg("migrate")) {
418 return self.migrate_wallet();
419 }
420 Ok(Evaluation::Output(fmt!(
421 "No recognised 'wallet' subcommand argument supplied.")))
422 }
423
424 fn migrate_wallet(&mut self) -> Outcome<Evaluation> {
425 let wallet_path = Path::new("./").join(app_const::WALLET_NAME);
426 if !wallet_path.is_file() {
427 return Ok(Evaluation::Output(fmt!(
428 "No wallet file to migrate at {:?}.", wallet_path)));
429 }
430 // Load the wallet file as raw Dat so we can read the legacy
431 // layout without depending on the old `Wallet<PH, D>` struct
432 // (which no longer exists in the source tree).
433 let text = res!(std::fs::read_to_string(&wallet_path));
434 let mut dat = res!(Dat::decode_string(&text));
435 if dat.kind() != Kind::OrdMap && dat.kind() != Kind::Map {
436 return Err(err!(
437 "Legacy wallet file at {:?} is not a map (kind={:?}).",
438 wallet_path, dat.kind();
439 Input, Invalid, Mismatch));
440 }
441 // If the file already has an "admins" list, it is already the
442 // new layout and there is nothing to do.
443 if let Ok(_) = dat.map_get_must(&dat!("admins")) {
444 return Ok(Evaluation::Output(fmt!(
445 "Wallet at {:?} is already in the admin-user layout.",
446 wallet_path)));
447 }
448 // Pull the current passphrase from the caller.
449 let pass = res!(UserInput::ask_for_secret(
450 Some("Enter the current wallet passphrase: "),
451 ));
452 let pass_bytes = pass.expose_secret().as_bytes();
453
454 // Verify the passphrase against the legacy `wallet_pass_hashes`
455 // ring buffer. We extract just the first (current) entry --
456 // older entries are historical and not used for verification.
457 let ring = res!(dat.map_remove_must(&dat!("wallet_pass_hashes")));
458 let current_hash_dat = res!(extract_legacy_current_passhash(ring));
459 let app_kdf_name = try_extract_dat!(
460 res!(current_hash_dat.map_get_must(&dat!("kdf_name"))),
461 Str,
462 );
463 let app_kdf_hash = try_extract_dat!(
464 res!(current_hash_dat.map_get_must(&dat!("kdf_hash"))),
465 Str,
466 );
467 let mut app_kdf = res!(KeyDerivationScheme::from_str(&app_kdf_name));
468 res!(app_kdf.decode_from_string(&app_kdf_hash));
469 if !res!(app_kdf.verify(pass_bytes)) {
470 return Ok(Evaluation::Output(fmt!(
471 "Passphrase rejected -- nothing migrated.")));
472 }
473
474 // Derive the current database encryption key via the legacy
475 // `app_hashes.default` KDF config. That derived key becomes
476 // the new wallet's master key, unchanged, so the on-disk
477 // Ozone data does not need to be re-encrypted.
478 let app_hashes = res!(dat.map_remove_must(&dat!("app_hashes")));
479 let default_entry = res!(app_hashes.map_get_must(&dat!("default"))).clone();
480 let db_kdf_name = try_extract_dat!(
481 res!(default_entry.map_get_must(&dat!("kdf_name"))),
482 Str,
483 );
484 let db_kdf_cfg = try_extract_dat!(
485 res!(default_entry.map_get_must(&dat!("kdf_cfg"))),
486 Str,
487 );
488 let mut db_kdf = res!(KeyDerivationScheme::from_str(&db_kdf_name));
489 res!(db_kdf.decode_cfg_from_string(&db_kdf_cfg));
490 res!(db_kdf.derive(pass_bytes));
491 let master_key = res!(db_kdf.get_hash()).to_vec();
492
493 // Preserve the existing metadata if present.
494 let metadata = match dat.map_remove(&dat!("metadata")) {
495 Ok(Some(d)) => try_extract_dat!(d, Map),
496 _ => DaticleMap::new(),
497 };
498
499 // Prompt for the new admin name, defaulting to the current
500 // unix user name or "operator".
501 print!("New admin name (default 'operator'): ");
502 {
503 use std::io::Write;
504 res!(std::io::stdout().flush());
505 }
506 let mut name_in = String::new();
507 res!(std::io::stdin().read_line(&mut name_in));
508 let admin_name = match name_in.trim() {
509 "" => "operator".to_string(),
510 s => s.to_string(),
511 };
512
513 // Build the fresh admin entry (wraps `master_key` under the
514 // same passphrase the caller just typed), assemble a new
515 // Wallet, and save it. The caller keeps using the same
516 // passphrase; nothing changes on the Ozone side.
517 let admin = res!(oxedyne_fe2o3_crypto::keystore::AdminUser::new(
518 admin_name.clone(),
519 pass_bytes,
520 &master_key,
521 oxedyne_fe2o3_crypto::keystore::DEFAULT_WALLET_KDF_NAME,
522 vec!["*".to_string()],
523 0,
524 ));
525 let new_wallet = Wallet::new(metadata, vec![admin], DaticleMap::new());
526
527 // Back up the old wallet first. A plain `fs::copy` would carry the
528 // source's mode, so the backup holds the same passphrase verifier
529 // and key-derivation material at a wider mode than the original.
530 let backup_path = Path::new("./").join(fmt!("{}.pre-admins", app_const::WALLET_NAME));
531 let old_wallet_bytes = match std::fs::read(&wallet_path) {
532 Ok(b) => b,
533 Err(e) => return Err(err!(e,
534 "Reading {:?} to back it up before the admin-user migration.", wallet_path;
535 IO, File, Read)),
536 };
537 res!(core_file::save_secret(&backup_path, &old_wallet_bytes));
538 res!(new_wallet.save_secret(
539 &wallet_path,
540 " ",
541 Some(EncoderConfig::<(), ()>::default()),
542 ));
543 {
544 let mut w = lock_write!(self.wallet);
545 *w = new_wallet;
546 }
547 audit::append(&admin_name, "wallet.migrate", "ok",
548 &fmt!("backup={:?}", backup_path));
549 Ok(Evaluation::Output(fmt!(
550 "Migrated wallet to the admin-user layout. New admin '{}' \
551 can unlock with the existing passphrase. Old wallet saved \
552 as {:?}.",
553 admin_name, backup_path,
554 )))
555 }
556
557 pub fn manage_admin(
558 &mut self,
559 _shell_cfg: &ShellConfig,
560 cmd: Option<&MsgCmd>,
561 )
562 -> Outcome<Evaluation>
563 {
564 let msg_cmd = match cmd {
565 Some(c) => c,
566 None => return Err(err!(
567 "admin requires a subcommand argument."; Invalid, Input, Missing)),
568 };
569 if msg_cmd.has_arg("list") {
570 return self.admin_list();
571 }
572 if msg_cmd.has_arg("passwd") {
573 return self.admin_passwd();
574 }
575 if msg_cmd.has_arg("add") {
576 let vals = res!(msg_cmd.get_arg_vals("add").with_len(1));
577 let name = try_extract_dat!(&vals[0], Str).clone();
578 let scopes: Vec<String> = match msg_cmd.get_arg_vals("scopes") {
579 Some(vs) if !vs.is_empty() => {
580 let s = try_extract_dat!(&vs[0], Str).clone();
581 s.split(',').map(|t| t.trim().to_string()).collect()
582 },
583 _ => Vec::new(),
584 };
585 let expires_in: u64 = match msg_cmd.get_arg_vals("expires-in") {
586 Some(vs) if !vs.is_empty() => match &vs[0] {
587 Dat::U64(n) => *n,
588 Dat::U32(n) => *n as u64,
589 _ => 0u64,
590 },
591 _ => 0u64,
592 };
593 let expires_at = if expires_in == 0 {
594 0
595 } else {
596 let now = std::time::SystemTime::now()
597 .duration_since(std::time::UNIX_EPOCH)
598 .map(|d| d.as_secs())
599 .unwrap_or(0);
600 now.saturating_add(expires_in)
601 };
602 return self.admin_add(&name, scopes, expires_at);
603 }
604 if msg_cmd.has_arg("remove") {
605 let vals = res!(msg_cmd.get_arg_vals("remove").with_len(1));
606 let name = try_extract_dat!(&vals[0], Str).clone();
607 return self.admin_remove(&name);
608 }
609 Ok(Evaluation::Output(fmt!(
610 "No recognised 'admin' subcommand argument supplied.")))
611 }
612
613 pub fn unseal(
614 &mut self,
615 _shell_cfg: &ShellConfig,
616 _cmd: Option<&MsgCmd>,
617 )
618 -> Outcome<Evaluation>
619 {
620 if self.db_enc_key.is_some() {
621 return Ok(Evaluation::Output(fmt!(
622 "Already unsealed by admin '{}'.", self.unlocked_admin_name,
623 )));
624 }
625 res!(self.require_master_key());
626 Ok(Evaluation::Output(fmt!(
627 "Unsealed by admin '{}'. The databases will open when the server \
628 starts.", self.unlocked_admin_name,
629 )))
630 }
631
632 pub fn require_master_key(&mut self) -> Outcome<Vec<u8>> {
633 if let Some(key) = &self.db_enc_key {
634 return Ok(key.clone());
635 }
636 let pass = match std::env::var(app_const::ADMIN_PASS_ENV) {
637 Ok(s) => Secret::new(s),
638 Err(_) => res!(UserInput::ask_for_secret(
639 Some("Enter an admin passphrase to unseal: "),
640 )),
641 };
642 let unlocked = {
643 let w = lock_read!(self.wallet);
644 res!(w.unlock(pass.expose_secret().as_bytes()))
645 };
646 let key = unlocked.master_key.expose_secret().clone();
647 self.db_enc_key = Some(key.clone());
648 self.unlocked_admin_name = unlocked.admin_name.clone();
649 self.unlocked_admin_scopes = unlocked.admin_scopes.clone();
650 info!("Wallet unlocked by admin '{}'.", unlocked.admin_name);
651 Ok(key)
652 }
653
654 fn admin_passwd(&mut self) -> Outcome<Evaluation> {
655 let master = res!(self.require_master_key());
656 let caller_name = self.unlocked_admin_name.clone();
657 if caller_name.is_empty() {
658 audit::append("(unknown)", "admin.passwd", "err",
659 "reason=no_caller_identity");
660 return Err(err!(
661 "No caller identity is known -- `admin --passwd` can only \
662 be invoked inside a running session that has already \
663 unlocked the wallet.";
664 Input, Invalid, Security));
665 }
666 let new_pass = res!(UserInput::create_pass(app_const::MAX_CREATE_PASS_ATTEMPTS));
667 let wallet_path = Path::new("./").join(app_const::WALLET_NAME);
668 {
669 let mut w = lock_write!(self.wallet);
670 if let Err(e) = w.change_password(
671 &caller_name,
672 &master,
673 new_pass.expose_secret().as_bytes(),
674 oxedyne_fe2o3_crypto::keystore::DEFAULT_WALLET_KDF_NAME,
675 ) {
676 audit::append(&caller_name, "admin.passwd", "err",
677 &fmt!("reason={}", e));
678 return Err(e);
679 }
680 res!(w.save_secret(
681 &wallet_path,
682 " ",
683 Some(EncoderConfig::<(), ()>::default()),
684 ));
685 }
686 audit::append(&caller_name, "admin.passwd", "ok", "self");
687 Ok(Evaluation::Output(fmt!(
688 "Password for admin '{}' rotated in place. The new password \
689 takes effect at the next Steel start-up; the running session \
690 keeps using the master key recovered at its original unlock.",
691 caller_name,
692 )))
693 }
694
695 fn admin_list(&self) -> Outcome<Evaluation> {
696 let mut lines = Vec::new();
697 lines.push(fmt!(
698 "{:<24} {:<12} {}",
699 "name", "expires_at", "scopes",
700 ));
701 let count;
702 {
703 let w = lock_read!(self.wallet);
704 for a in w.admins() {
705 let expiry = if a.expires_at == 0 {
706 "never".to_string()
707 } else {
708 fmt!("{}", a.expires_at)
709 };
710 lines.push(fmt!(
711 "{:<24} {:<12} {}",
712 a.name, expiry, a.scopes.join(","),
713 ));
714 }
715 count = w.admins().len();
716 }
717 audit::append("(anon)", "admin.list", "ok",
718 &fmt!("count={}", count));
719 Ok(Evaluation::Output(lines.join("\n")))
720 }
721
722 fn unlocked_has_admin_scope(&self) -> bool {
723 self.unlocked_admin_scopes.iter()
724 .any(|s| s == "*" || s == "admin")
725 }
726
727 fn admin_add(
728 &mut self,
729 new_name: &str,
730 new_scopes: Vec<String>,
731 expires_at: u64,
732 )
733 -> Outcome<Evaluation>
734 {
735 // Unseal first: the caller's identity and scopes are exactly what
736 // the unlock establishes, so there is nothing to authorise until
737 // it has happened.
738 let master = res!(self.require_master_key());
739 let caller_name = self.unlocked_admin_name.clone();
740 if !self.unlocked_has_admin_scope() {
741 audit::append(&caller_name, "admin.add", "err",
742 &fmt!("target={} reason=caller_scope", new_name));
743 return Err(err!(
744 "Admin '{}' does not hold the 'admin' scope; cannot \
745 enrol new admins.", caller_name;
746 Input, Invalid, Security));
747 }
748 let new_pass = res!(UserInput::create_pass(app_const::MAX_CREATE_PASS_ATTEMPTS));
749 let wallet_path = Path::new("./").join(app_const::WALLET_NAME);
750 {
751 let mut w = lock_write!(self.wallet);
752 if let Err(e) = w.enrol(
753 &master,
754 new_name,
755 new_pass.expose_secret().as_bytes(),
756 new_scopes.clone(),
757 expires_at,
758 oxedyne_fe2o3_crypto::keystore::DEFAULT_WALLET_KDF_NAME,
759 ) {
760 audit::append(&caller_name, "admin.add", "err",
761 &fmt!("target={} reason={}", new_name, e));
762 return Err(e);
763 }
764 res!(w.save_secret(
765 &wallet_path,
766 " ",
767 Some(EncoderConfig::<(), ()>::default()),
768 ));
769 }
770 audit::append(&caller_name, "admin.add", "ok",
771 &fmt!("target={} scopes={} expires_at={}",
772 new_name, new_scopes.join(","), expires_at));
773 Ok(Evaluation::Output(fmt!(
774 "Added admin '{}'.", new_name,
775 )))
776 }
777
778 fn admin_remove(&mut self, target_name: &str) -> Outcome<Evaluation> {
779 // As in `admin_add`: the unlock is what establishes who the caller
780 // is, so it has to precede the scope check.
781 res!(self.require_master_key());
782 let caller_name = self.unlocked_admin_name.clone();
783 if !self.unlocked_has_admin_scope() {
784 audit::append(&caller_name, "admin.remove", "err",
785 &fmt!("target={} reason=caller_scope", target_name));
786 return Err(err!(
787 "Admin '{}' does not hold the 'admin' scope; cannot \
788 remove admin entries.", caller_name;
789 Input, Invalid, Security));
790 }
791 let wallet_path = Path::new("./").join(app_const::WALLET_NAME);
792 {
793 let mut w = lock_write!(self.wallet);
794 if let Err(e) = w.remove_by_name(target_name) {
795 audit::append(&caller_name, "admin.remove", "err",
796 &fmt!("target={} reason={}", target_name, e));
797 return Err(e);
798 }
799 res!(w.save_secret(
800 &wallet_path,
801 " ",
802 Some(EncoderConfig::<(), ()>::default()),
803 ));
804 }
805 audit::append(&caller_name, "admin.remove", "ok",
806 &fmt!("target={}", target_name));
807 Ok(Evaluation::Output(fmt!(
808 "Removed admin '{}'.", target_name,
809 )))
810 }
811
812 pub fn manage_certificates(
813 &mut self,
814 _shell_cfg: &ShellConfig,
815 cmd: Option<&MsgCmd>,
816 )
817 -> Outcome<Evaluation>
818 {
819 if let Some(msg_cmd) = cmd {
820 if msg_cmd.has_args() {
821 if res!(msg_cmd.has_only_arg("create-dev")) {
822 info!("Generating self-signed development certificates...");
823 res!(Certificate::new_dev(
824 &ServerConfig::default(),
825 &Path::new(&self.app_cfg.app_root).normalise().absolute(),
826 ));
827 return Ok(Evaluation::Output(fmt!(
828 "Self-signed development certificates generated in {}/tls/{}",
829 self.app_cfg.app_root,
830 srv_const::TLS_DIR_DEV,
831 )));
832 }
833 } else {
834 let avail_args = if let Some(cmd) = msg_cmd.syntax.get_cmd(&*msg_cmd.name) {
835 cmd.collect_short_arg_names()
836 .iter()
837 .map(|s| fmt!("-{}", s))
838 .collect::<Vec<_>>()
839 .join(" ")
840 } else {
841 fmt!("<no args>")
842 };
843 return Ok(Evaluation::Error(fmt!(
844 "Must use one of '{}' for command '{}'. Type 'help' for more info.",
845 avail_args, msg_cmd.name,
846 )));
847 }
848 }
849 Ok(Evaluation::None)
850 }
851
852 pub fn manage_acme(
853 &mut self,
854 _shell_cfg: &ShellConfig,
855 cmd: Option<&MsgCmd>,
856 )
857 -> Outcome<Evaluation>
858 {
859 let server_cfg = res!(ServerConfig::from_datmap(self.app_cfg.server_cfg.clone()));
860 let acme_cfg = res!(server_cfg.get_acme());
861 let vhosts = res!(server_cfg.get_vhosts());
862
863 if let Some(msg_cmd) = cmd {
864 if msg_cmd.has_args() {
865 if res!(msg_cmd.has_only_arg("status")) {
866 let mut lines = Vec::new();
867 lines.push(fmt!("ACME enabled: {}", acme_cfg.enabled));
868 lines.push(fmt!("Directory URL: {}", acme_cfg.directory_url));
869 lines.push(fmt!("Contact email: {}",
870 if acme_cfg.contact_email.is_empty() {
871 fmt!("(not set)")
872 } else {
873 acme_cfg.contact_email.clone()
874 }));
875 lines.push(fmt!("Cache dir: {}", acme_cfg.cache_dir_rel));
876 lines.push(fmt!("Vhost hostnames:"));
877 for vh in &vhosts {
878 lines.push(fmt!(" - {}", vh.hostnames.join(", ")));
879 }
880 return Ok(Evaluation::Output(lines.join("\n")));
881 } else if res!(msg_cmd.has_only_arg("renew")) {
882 let root = Path::new(&self.app_cfg.app_root).normalise().absolute();
883 let cache_dir = res!(acme_cfg.get_cache_dir(&root));
884 info!("Clearing ACME cache at {:?} to force renewal on next start-up.",
885 cache_dir);
886 match std::fs::remove_dir_all(&cache_dir) {
887 Ok(()) => (),
888 Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
889 Err(e) => return Err(err!(e,
890 "Failed to clear ACME cache at {:?}.", cache_dir;
891 IO, File)),
892 }
893 res!(std::fs::create_dir_all(&cache_dir));
894 return Ok(Evaluation::Output(fmt!(
895 "ACME cache cleared at {:?}. Restart the server to re-issue.",
896 cache_dir,
897 )));
898 }
899 } else {
900 return Ok(Evaluation::Error(fmt!(
901 "Use 'acme -s' for status or 'acme -r' to schedule renewal.",
902 )));
903 }
904 }
905 Ok(Evaluation::None)
906 }
907}
908
909fn extract_legacy_current_passhash(ring: Dat) -> Outcome<Dat> {
910 // Tuples round-trip as `Dat::Tup2` values that we destructure
911 // with `try_extract_tup2dat`. The first element is the slot
912 // list, the second is the index.
913 let mut parts = oxedyne_fe2o3_jdat::try_extract_tup2dat!(ring);
914 let index: u64 = match parts[1].extract() {
915 Dat::U64(n) => n,
916 other => return Err(err!(
917 "Legacy ring buffer index must be u64 (got {:?}).", other.kind();
918 Invalid, Input)),
919 };
920 let list = oxedyne_fe2o3_jdat::try_extract_dat!(parts[0].extract(), Vek);
921 let slot = ok!(list.into_iter().nth(index as usize).ok_or_else(|| err!(
922 "Legacy ring buffer index {} out of range.", index;
923 Input, Invalid, Mismatch)));
924 // Each slot is an `Opt<Tup2(data, timestamp)>`. The caller wants
925 // the `data` daticle, which is the kdf map.
926 let some = match slot {
927 Dat::Opt(inner) => match *inner {
928 Some(d) => d,
929 None => return Err(err!(
930 "Legacy ring buffer current slot is None.";
931 Input, Missing)),
932 },
933 other => return Err(err!(
934 "Legacy ring buffer slot must be Opt (got {:?}).", other.kind();
935 Input, Invalid, Mismatch)),
936 };
937 let mut slot_parts = oxedyne_fe2o3_jdat::try_extract_tup2dat!(some);
938 Ok(slot_parts[0].extract())
939}
940