Skip to main content

pubhubs/cli/
tools.rs

1use crate::common::elgamal::Encoding as _;
2use anyhow::Result;
3
4#[derive(clap::Args, Debug)]
5pub struct ToolsArgs {
6    #[command(subcommand)]
7    command: Commands,
8}
9
10impl ToolsArgs {
11    pub fn run(self, _spec: &mut clap::Command) -> Result<()> {
12        match self.command {
13            Commands::Generate(args) => args.run(),
14            Commands::YiviEpoch(args) => args.run(),
15        }
16    }
17}
18
19#[derive(clap::Subcommand, Debug)]
20enum Commands {
21    /// Generates identifiers and/or key material
22    Generate(generate::Args),
23
24    /// Prints information about the current Yivi epoch
25    YiviEpoch(YiviEpochArgs),
26}
27
28#[derive(clap::Args, Debug)]
29struct YiviEpochArgs {
30    /// Print information about the NUMBERth yivi epoch
31    #[arg(long, value_name = "NUMBER", conflicts_with = "at")]
32    nr: Option<u64>,
33
34    /// Print information about the yivi epoch at the given TIMESTAMP such as '2025-12-17 15:15:15'
35    #[arg(
36        long,
37        value_name = "TIMESTAMP",
38        value_parser = humantime::parse_rfc3339_weak,
39        conflicts_with = "nr"
40    )]
41    at: Option<std::time::SystemTime>,
42}
43
44impl YiviEpochArgs {
45    fn run(self) -> Result<()> {
46        let epoch = if let Some(nr) = self.nr {
47            crate::servers::yivi::Epoch::with_seqnr(nr)?
48        } else if let Some(at) = self.at {
49            let nd: crate::api::NumericDate = at.try_into()?;
50            crate::servers::yivi::Epoch::from(nd)
51        } else {
52            crate::servers::yivi::Epoch::current()
53        };
54
55        print!("{}", epoch);
56
57        Ok(())
58    }
59}
60
61/// Implementation details of [`Commands::Generate`].
62mod generate {
63    use super::*;
64
65    #[derive(clap::Args, Debug)]
66    pub(super) struct Args {
67        #[command(subcommand)]
68        command: Commands,
69    }
70
71    impl Args {
72        pub(super) fn run(self) -> Result<()> {
73            match self.command {
74                Commands::Id(args) => args.run(),
75                Commands::Scalar(args) => args.run(),
76                Commands::SigningKey(args) => args.run(),
77                Commands::DecapKey(args) => args.run(),
78            }
79        }
80    }
81
82    #[derive(clap::Subcommand, Debug)]
83    enum Commands {
84        /// Generate a random identifier for e.g. a hub, attribute type, ...
85        Id(IdArgs),
86
87        /// Generate a random ristretto25519 scalar to be used e.g. as elgamal private key
88        Scalar(ScalarArgs),
89
90        /// Generate a random ed25519 signing key
91        SigningKey(SigningKeyArgs),
92
93        /// Generate a decapsulation key
94        DecapKey(DecapKeyArgs),
95    }
96
97    #[derive(clap::Args, Debug)]
98    struct IdArgs {}
99
100    impl IdArgs {
101        fn run(self) -> Result<()> {
102            println!("{}", crate::id::Id::random());
103
104            Ok(())
105        }
106    }
107
108    #[derive(clap::Args, Debug)]
109    struct ScalarArgs {}
110
111    impl ScalarArgs {
112        fn run(self) -> Result<()> {
113            let pk = crate::common::elgamal::PrivateKey::random();
114
115            println!("x (private key): {}", pk.to_hex());
116            println!("xB (public key): {}", pk.public_key().to_hex());
117
118            Ok(())
119        }
120    }
121
122    #[derive(clap::Args, Debug)]
123
124    struct SigningKeyArgs {}
125
126    impl SigningKeyArgs {
127        fn run(self) -> Result<()> {
128            let sk = crate::api::SigningKey::generate()
129                .map_err(|_| anyhow::anyhow!("failed to generate signing key"))?;
130
131            println!("  signing key: {}", serde_json::to_string(&sk.encode())?);
132            println!(
133                "verifying key: {}",
134                serde_json::to_string(&sk.verifying_key().encode())?
135            );
136
137            Ok(())
138        }
139    }
140
141    #[derive(clap::Args, Debug)]
142    struct DecapKeyArgs {}
143
144    impl DecapKeyArgs {
145        fn run(self) -> Result<()> {
146            let dk = crate::common::kem::DecapKey::generate()
147                .map_err(|_| anyhow::anyhow!("failed to generate decapsulation key"))?;
148
149            let encap_key_id = dk
150                .encap_key()
151                .encode()
152                .map_err(|_| anyhow::anyhow!("failed to encode encapsulation key"))?
153                .id();
154
155            let decap_key = dk
156                .encode()
157                .map_err(|_| anyhow::anyhow!("failed to encode decapsulation key"))?;
158
159            println!("# corresponding encapsulation key id: {encap_key_id}");
160            println!("{}", decap_key_config_snippet(&decap_key)?);
161
162            Ok(())
163        }
164    }
165
166    /// Renders `decap_key = { ... }` as a multi-line inline table (so it can be pasted under a
167    /// transcryptor or authentication server), wrapping the long base64 fields across lines.  The
168    /// structure is written via [`toml_writer`]; only the `\`-folding - which no TOML serializer
169    /// does for us - is done here.
170    fn decap_key_config_snippet(decap_key: &crate::common::kem::DecapKeyBytes) -> Result<String> {
171        use core::fmt::Write as _;
172        use toml_writer::{TomlStringBuilder, TomlWrite as _};
173
174        /// Target maximum line width.
175        const COLUMN_WIDTH: usize = 100;
176        /// Indentation of a value's wrapped base64 lines.
177        const INDENT: &str = "    ";
178
179        // Leave room for the indent and either a trailing `\` or the closing `"""`.
180        let chunk_width = COLUMN_WIDTH - INDENT.len() - 3;
181
182        /// A `\`-line-folded multi-line basic string.  The opening `"""\` and each trailing `\`
183        /// swallow the following newline and indentation, so this reparses to exactly `s` while
184        /// every printed line stays indented and within the column width.
185        fn folded(s: &str, indent: &str, chunk_width: usize) -> String {
186            let mut out = String::from("\"\"\"\\\n");
187            let mut rest = s;
188            while rest.len() > chunk_width {
189                let (head, tail) = rest.split_at(chunk_width);
190                out.push_str(indent);
191                out.push_str(head);
192                out.push_str("\\\n");
193                rest = tail;
194            }
195            out.push_str(indent);
196            out.push_str(rest);
197            out.push_str("\"\"\"");
198            out
199        }
200
201        // The fields are private, so enumerate them through `toml::Value`.
202        let value = toml::Value::try_from(decap_key)
203            .map_err(|_| anyhow::anyhow!("failed to serialize decapsulation key"))?;
204        let table = value
205            .as_table()
206            .expect("a decapsulation key serializes to a table");
207
208        let mut out = String::new();
209        out.key("decap_key")?;
210        out.space()?;
211        out.keyval_sep()?;
212        out.space()?;
213        out.open_inline_table()?;
214        out.newline()?;
215        for (field, value) in table.iter() {
216            let value = value
217                .as_str()
218                .expect("decapsulation key fields serialize to strings");
219
220            out.write_str("  ")?;
221            out.key(field.as_str())?;
222            out.space()?;
223            out.keyval_sep()?;
224            out.space()?;
225
226            if value.len() <= chunk_width {
227                out.value(TomlStringBuilder::new(value).as_basic())?;
228            } else {
229                out.write_str(&folded(value, INDENT, chunk_width))?;
230            }
231
232            out.val_sep()?;
233            out.newline()?;
234        }
235        out.close_inline_table()?;
236
237        Ok(out)
238    }
239
240    #[cfg(test)]
241    mod tests {
242        use super::*;
243
244        #[test]
245        fn decap_key_snippet_reparses() {
246            let decap_key = crate::common::kem::DecapKey::generate()
247                .unwrap()
248                .encode()
249                .unwrap();
250
251            let snippet = decap_key_config_snippet(&decap_key).unwrap();
252
253            #[derive(serde::Deserialize)]
254            struct Parsed {
255                decap_key: crate::common::kem::DecapKeyBytes,
256            }
257            let parsed: Parsed = toml::from_str(&snippet).expect("snippet should reparse");
258
259            // `DecapKeyBytes` isn't `PartialEq`, so compare via re-serialization.
260            assert_eq!(
261                serde_json::to_string(&parsed.decap_key).unwrap(),
262                serde_json::to_string(&decap_key).unwrap(),
263            );
264        }
265    }
266}