| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- 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 <file>` writes the full config — public and secret
- * halves — directly to `<file>` with mode 0600.
- *
- * `--out <file>` 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 <file>]\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 <file> 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 <file>\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;
- }
- }
- }
- }
|