about summary refs log tree commit diff stats
path: root/crates/rocie-server/src/main.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-03-19 05:00:15 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-03-19 05:00:15 +0100
commitc1e70050872f398a7dccff5818b98f6eb100710c (patch)
treeb0b44488650862aa369b67d51bb70209f9f2f78b /crates/rocie-server/src/main.rs
parentbuild(rocie-{client,server}): Mark as publishable (diff)
downloadserver-c1e70050872f398a7dccff5818b98f6eb100710c.zip
feat(rocie-server/cli): Make the secret key for identity handling persist-able
Diffstat (limited to '')
-rw-r--r--crates/rocie-server/src/main.rs232
1 files changed, 225 insertions, 7 deletions
diff --git a/crates/rocie-server/src/main.rs b/crates/rocie-server/src/main.rs
index 5f3b0ff..c57c3a2 100644
--- a/crates/rocie-server/src/main.rs
+++ b/crates/rocie-server/src/main.rs
@@ -5,6 +5,8 @@ use actix_web::{
     web::Data,
 };
 use clap::Parser;
+use log::warn;
+use tokio::{fs::File, io::AsyncReadExt};
 use utoipa::OpenApi;
 
 use crate::cli::{CliArgs, Command};
@@ -78,12 +80,6 @@ async fn main() -> Result<(), std::io::Error> {
 
     env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
 
-    // When using `Key::generate()` it is important to initialize outside of the
-    // `HttpServer::new` closure. When deployed the secret key should be read from a
-    // configuration file or environment variables.
-    // TODO: Load from a config file. <2025-12-07>
-    let secret_key = Key::generate();
-
     let args = CliArgs::parse();
 
     match args.command {
@@ -92,6 +88,7 @@ async fn main() -> Result<(), std::io::Error> {
             port,
             db_path,
             print_port,
+            secret_key_file,
         } => {
             let data = Data::new(
                 app::App::new(db_path)
@@ -99,6 +96,33 @@ async fn main() -> Result<(), std::io::Error> {
                     .map_err(|err| std::io::Error::other(main::Error::AppInit(err)))?,
             );
 
+            let secret_key = if let Some(path) = secret_key_file {
+                let mut file = File::open(&path).await.map_err(|error| {
+                    std::io::Error::other(secret_key_read::Error::FileOpen {
+                        path: path.clone(),
+                        error,
+                    })
+                })?;
+
+                let mut buf = Vec::with_capacity(64 * 2);
+                file.read_to_end(&mut buf).await.map_err(|error| {
+                    std::io::Error::other(secret_key_read::Error::FileRead {
+                        error,
+                        path: path.clone(),
+                    })
+                })?;
+
+                let contents = String::from_utf8(buf).map_err(|error| {
+                    std::io::Error::other(secret_key_read::Error::Utf8 { error, path })
+                })?;
+
+                Key::from(&parse_hex_key(&contents))
+            } else {
+                // When using `Key::generate()` it is important to initialize outside of the
+                // `HttpServer::new` closure.
+                Key::generate()
+            };
+
             let srv = HttpServer::new(move || {
                 App::new()
                     .wrap(Logger::new(
@@ -140,15 +164,209 @@ async fn main() -> Result<(), std::io::Error> {
             println!("{}", openapi.to_pretty_json().expect("Comp-time constant"));
             Ok(())
         }
+        Command::GenerateKey => {
+            let key = Key::generate();
+
+            print!("{}", format_hex_val(key.master()));
+
+            Ok(())
+        }
+    }
+}
+
+fn parse_hex_key(key: &str) -> [u8; 64] {
+    let mut out = [0u8; 64];
+
+    let back = parse_hex_string(key);
+
+    if back.len() != 64 {
+        warn!(
+            "Secret key is not 64 bytes long (len: {}), padding with zeros.",
+            back.len()
+        );
+    }
+
+    for (index, b) in back.iter().enumerate() {
+        out[index] = *b;
+    }
+
+    out
+}
+
+fn format_hex_val(key: &[u8]) -> String {
+    fn value_to_hex(value: u8) -> char {
+        match value {
+            0 => '0',
+            1 => '1',
+            2 => '2',
+            3 => '3',
+            4 => '4',
+            5 => '5',
+            6 => '6',
+            7 => '7',
+            8 => '8',
+            9 => '9',
+            10 => 'a',
+            11 => 'b',
+            12 => 'c',
+            13 => 'd',
+            14 => 'e',
+            15 => 'f',
+            _ => unreachable!("The 'value' is a u4"),
+        }
+    }
+
+    let mut out = String::new();
+
+    for val in key {
+        let first = value_to_hex(val >> 4);
+        let second = value_to_hex(val & 0b0000_1111);
+
+        out.push(first);
+        out.push(second);
+    }
+
+    out
+}
+
+fn parse_hex_string(input: &str) -> Vec<u8> {
+    let mut out = vec![0u8; input.len().div_ceil(2)];
+
+    let i1 = input.chars().step_by(2);
+    let mut i2 = input.chars().skip(1).step_by(2);
+
+    for (index, b1) in i1.enumerate() {
+        if let Some(b2) = i2.next() {
+            out[index] = parse_hex_pair((b1, b2));
+        } else {
+            // We only have b1 left
+            out[index] = parse_hex_digit(b1);
+            break;
+        }
+    }
+
+    out
+}
+
+fn parse_hex_pair(digit: (char, char)) -> u8 {
+    let first = parse_hex_digit(digit.0);
+    let second = parse_hex_digit(digit.1);
+
+    (first << 4) | second
+}
+
+fn parse_hex_digit(digit: char) -> u8 {
+    match digit.to_ascii_uppercase() {
+        '0' => 0,
+        '1' => 1,
+        '2' => 2,
+        '3' => 3,
+        '4' => 4,
+        '5' => 5,
+        '6' => 6,
+        '7' => 7,
+        '8' => 8,
+        '9' => 9,
+        'A' => 10,
+        'B' => 11,
+        'C' => 12,
+        'D' => 13,
+        'E' => 14,
+        'F' => 15,
+        _ => unreachable!("Invalid hex char: {digit:?}"),
     }
 }
 
 pub(crate) mod main {
-    use crate::app::app_create;
+    use crate::{app::app_create, secret_key_read};
 
     #[derive(thiserror::Error, Debug)]
     pub(crate) enum Error {
         #[error("Failed to initialize shared application state: {0}")]
         AppInit(#[from] app_create::Error),
+
+        #[error("Failed to load secret key from --secret-key-file arg: {0}")]
+        SecretFileRead(#[from] secret_key_read::Error),
+    }
+}
+
+pub(crate) mod secret_key_read {
+    use std::{path::PathBuf, string::FromUtf8Error};
+
+    use tokio::io;
+
+    #[derive(thiserror::Error, Debug)]
+    pub(crate) enum Error {
+        #[error("Failed to open the secret key file at `{path}`: {error}")]
+        FileOpen { path: PathBuf, error: io::Error },
+
+        #[error("Failed to read the contents of the secret key file at `{path}`: {error}")]
+        FileRead { path: PathBuf, error: io::Error },
+
+        #[error(
+            "Failed to interprete seceret key file (at `{path}`) contents as utf8 string: {error}"
+        )]
+        Utf8 { path: PathBuf, error: FromUtf8Error },
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::{format_hex_val, parse_hex_key, parse_hex_pair, parse_hex_string};
+
+    #[test]
+    fn test_hex_parse() {
+        let input = "4c6541594a53a79b4649cce91610271e0b477748477dc89f350f8c3bbfc2f1a3b67ae51c56d2286006e070022529ed5b586d114985d05558cd2200bbc5d641c8";
+
+        let out = parse_hex_key(input);
+
+        assert_eq!(
+            out,
+            [
+                76, 101, 65, 89, 74, 83, 167, 155, 70, 73, 204, 233, 22, 16, 39, 30, 11, 71, 119,
+                72, 71, 125, 200, 159, 53, 15, 140, 59, 191, 194, 241, 163, 182, 122, 229, 28, 86,
+                210, 40, 96, 6, 224, 112, 2, 37, 41, 237, 91, 88, 109, 17, 73, 133, 208, 85, 88,
+                205, 34, 0, 187, 197, 214, 65, 200
+            ]
+        );
+
+        assert_eq!(format_hex_val(&out), input.to_owned());
+    }
+
+    #[test]
+    fn test_hex_parse_basic() {
+        macro_rules! test {
+            ($input:literal -> $expected:literal) => {
+                let output = parse_hex_pair((
+                    $input.chars().next().unwrap(),
+                    $input.chars().skip(1).next().unwrap(),
+                ));
+
+                assert_eq!(output, $expected);
+            };
+        }
+
+        test!("ff" -> 255);
+        test!("10" -> 16);
+        test!("05" -> 5);
+        test!("0f" -> 15);
+        test!("af" -> 175);
+    }
+
+    #[test]
+    fn test_hex_parse_str() {
+        macro_rules! test {
+            ($input:literal -> [$($expected:literal),*]) => {
+                let output = parse_hex_string($input);
+
+                assert_eq!(output, vec![$($expected),*]);
+            };
+        }
+
+        test!("ffff" -> [255, 255]);
+        test!("1020" -> [16,32]);
+        test!("05ff" -> [5, 255]);
+        test!("0ff" -> [15, 15]);
+        test!("afb" -> [175, 11]);
     }
 }