using Sodium; namespace Statum.Tools { /** * `statum-genkeys` — generates the static key pairs for signing and * encryption for the `statum` section of `web-config.json`. * * With these keys configured, signed frames and encrypted private blobs * survive a server restart (the same keys verify/decrypt across restarts). * * The secret halves are key material. By default they are never written to * stdout (shell history, terminal scrollback, CI logs and `2>&1 | tee` * captures would all acquire them — SA-12): stdout carries only the PUBLIC * halves, and `--out ` writes the full config — public and secret * halves — directly to `` with mode 0600. * * `--out ` writes the ready-to-use `web-config.json` (creating * parent directories as needed). Use it instead of shell redirection: * * ```bash * statum-genkeys --out web-config.json * ``` */ public class Genkeys : Object { private const string USAGE = "usage: statum-genkeys [--out ]\n" + "\n" + "Generates the static signing/encryption key pairs for web-config.json.\n" + "Without --out, prints only the PUBLIC key halves to stdout (SA-12: secret\n" + "halves are never printed). With --out, writes the full config — public and\n" + "secret halves — to with mode 0600.\n"; public static int main(string[] args) { string? out_path = null; for (int i = 1; i < args.length; i++) { var arg = args[i]; if (arg == "--out" || arg == "-o") { if (i + 1 >= args.length) { stderr.printf("statum-genkeys: --out requires a file argument\n%s", USAGE); return 2; } out_path = args[++i]; } else if (arg == "--help" || arg == "-h") { stdout.printf("%s", USAGE); return 0; } else { stderr.printf("statum-genkeys: unknown argument \"%s\"\n%s", arg, USAGE); return 2; } } // Signing (Ed25519) key pair. var sign_sk = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES]; var sign_pk = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES]; Sodium.Asymmetric.Signing.generate_keypair(sign_pk, sign_sk); // Encryption signing (Ed25519) key pair. var esign_sk = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES]; var esign_pk = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES]; Sodium.Asymmetric.Signing.generate_keypair(esign_pk, esign_sk); // Encryption sealing (X25519) key pair. var seal_sk = new uint8[Sodium.Asymmetric.Sealing.SECRET_KEY_BYTES]; var seal_pk = new uint8[Sodium.Asymmetric.Sealing.PUBLIC_KEY_BYTES]; Sodium.Asymmetric.Sealing.generate_keypair(seal_pk, seal_sk); if (out_path != null) { // Full config (secrets included) goes to the file only, mode 0600. var full = new StringBuilder(); full.append("{\n"); full.append(" \"statum\": {\n"); full.append_printf(" \"signing_secret_key\": \"%s\",\n", Base64.encode(sign_sk)); full.append_printf(" \"signing_public_key\": \"%s\",\n", Base64.encode(sign_pk)); full.append_printf(" \"encryption_signing_secret_key\": \"%s\",\n", Base64.encode(esign_sk)); full.append_printf(" \"encryption_signing_public_key\": \"%s\",\n", Base64.encode(esign_pk)); full.append_printf(" \"encryption_sealing_secret_key\": \"%s\",\n", Base64.encode(seal_sk)); full.append_printf(" \"encryption_sealing_public_key\": \"%s\"\n", Base64.encode(seal_pk)); full.append(" }\n"); full.append("}\n"); if (!write_private_file((!)out_path, full.str)) { return 1; } stdout.printf("Wrote full key material to %s (mode 0600).\n", (!)out_path); stdout.printf("The secret halves are in %s only — they are never printed to stdout.\n", (!)out_path); print_public_halves(sign_pk, esign_pk, seal_pk); return 0; } // No --out: keep the JSON shape scriptable, but carry PUBLIC halves // only. Anything that parsed this output for secrets must use // --out (see SA-12). stdout.printf("{\n"); stdout.printf(" \"statum\": {\n"); stdout.printf(" \"signing_public_key\": \"%s\",\n", Base64.encode(sign_pk)); stdout.printf(" \"encryption_signing_public_key\": \"%s\",\n", Base64.encode(esign_pk)); stdout.printf(" \"encryption_sealing_public_key\": \"%s\"\n", Base64.encode(seal_pk)); stdout.printf(" }\n"); stdout.printf("}\n"); stdout.printf("# Secret key halves are not printed. Use:\n"); stdout.printf("# statum-genkeys --out \n"); stdout.printf("# to write the full config (public + secret) at mode 0600.\n"); return 0; } private static void print_public_halves(uint8[] sign_pk, uint8[] esign_pk, uint8[] seal_pk) { stdout.printf("signing_public_key: %s\n", Base64.encode(sign_pk)); stdout.printf("encryption_signing_public_key: %s\n", Base64.encode(esign_pk)); stdout.printf("encryption_sealing_public_key: %s\n", Base64.encode(seal_pk)); } /** * Writes `contents` to `path` with mode 0600, creating parent * directories as needed. `File.replace` gives a fresh file, so the * restrictive mode is applied immediately after the write. */ private static bool write_private_file(string path, string contents) { try { var file = File.new_for_path(path); var parent = file.get_parent(); if (parent != null && !((!)parent).query_exists()) { try { ((!)parent).make_directory_with_parents(null); } catch (GLib.Error e) { if (!file.query_exists()) { throw e; } } } var stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE)); stream.put_string(contents); stream.close(); file.set_attribute_uint32(FileAttribute.UNIX_MODE, 0600, FileQueryInfoFlags.NONE, null); return true; } catch (GLib.Error e) { stderr.printf("statum-genkeys: %s\n", e.message); return false; } } } }