using GLib; using Json; namespace Spry.Cli { /** * `spry keys` — runs `statum-genkeys` and merges its key material into * the "statum" section of the application's `web-config.json`. * * Key material is maintained as PAIRS (a secret and its public half): * when either half of a pair is missing the whole pair is regenerated, * so a half-deleted file is repaired with a matching pair rather than a * mismatched one. Any run that (re)writes key material prints a * prominent warning: regenerating a pair invalidates existing sessions * and sealed blobs. Complete pairs and every other configuration * section are left untouched. */ public class Keys : GLib.Object { /** * The static key fields Statum reads from the "statum" section, * ordered secret/public pair-wise so missing halves can be repaired * by regenerating the whole pair. */ public const string[] STATUM_KEYS = { "signing_secret_key", "signing_public_key", "encryption_signing_secret_key", "encryption_signing_public_key", "encryption_sealing_secret_key", "encryption_sealing_public_key" }; /** * Fills any missing "statum" key pairs in `config_path`, creating * the file (and the section) when absent, and repairs half-present * pairs by regenerating them whole. Does nothing when every key is * already configured. */ public static void run(string config_path, string? tools_dir, string? libdir) throws Error { if (has_all_keys(config_path)) { stdout.printf("All static keys are already configured in %s — nothing to do.\n", config_path); return; } var generated = spawn_genkeys(tools_dir, libdir); string[] repaired; var added = merge(config_path, generated, out repaired); foreach (var key in added) { stdout.printf("Added %s to the statum section of %s\n", key, config_path); } foreach (var pair in repaired) { stdout.printf("Repaired an incomplete pair in %s — %s were regenerated\n", config_path, pair); } stderr.printf("*** WARNING: new static key material was generated in %s.\n", config_path); stderr.printf("*** Existing sessions and sealed blobs are invalidated by this change.\n"); } private static bool has_all_keys(string config_path) { if (!FileUtils.test(config_path, FileTest.EXISTS)) { return false; } try { var parser = new Parser(); parser.load_from_file(config_path); var root = parser.get_root(); if (root == null || root.get_object() == null || !((!)root).get_object().has_member("statum")) { return false; } var statum = ((!)root).get_object().get_object_member("statum"); foreach (var key in STATUM_KEYS) { if (!statum.has_member(key) || statum.get_string_member(key) == null || ((!)statum.get_string_member(key)).length == 0) { return false; } } return true; } catch (Error e) { return false; } } private static Json.Object spawn_genkeys(string? tools_dir, string? libdir) throws Error { var tool = Tools.resolve("statum-genkeys", tools_dir); var launcher = new SubprocessLauncher(SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_PIPE); Tools.apply_env(launcher, libdir); var process = launcher.spawnv({ tool }); string stdout_buf; string stderr_buf; process.communicate_utf8(null, null, out stdout_buf, out stderr_buf); if (!process.get_successful()) { throw Generator.cli_error("statum-genkeys failed: %s", (stderr_buf ?? "").strip()); } var parser = new Parser(); parser.load_from_data(stdout_buf ?? ""); var root = parser.get_root(); if (root == null || root.get_object() == null || !((!)root).get_object().has_member("statum")) { throw Generator.cli_error("unexpected statum-genkeys output"); } return ((!)root).get_object().get_object_member("statum"); } /** * Merges the generated material pair-wise: complete pairs are kept, * pairs with a missing half are regenerated whole (`repaired` lists * them as `"secret + public"` labels), entirely absent pairs are * added (`added` lists their keys). The file is rewritten with mode * 0600 because it carries key material. */ private static string[] merge(string config_path, Json.Object generated, out string[] repaired) throws Error { repaired = new string[0]; var root = new Json.Object(); if (FileUtils.test(config_path, FileTest.EXISTS)) { var parser = new Parser(); try { parser.load_from_file(config_path); } catch (Error e) { throw Generator.cli_error("%s is not valid JSON: %s", config_path, e.message); } var existing = parser.get_root(); if (existing == null || existing.get_object() == null) { throw Generator.cli_error("%s has no JSON object at the top level", config_path); } root = ((!)existing).get_object(); } if (!root.has_member("statum")) { root.set_object_member("statum", new Json.Object()); } var statum = root.get_object_member("statum"); var added = new string[0]; var repaired_pairs = new string[0]; for (int i = 0; i + 1 < STATUM_KEYS.length; i += 2) { var secret_key = STATUM_KEYS[i]; var public_key = STATUM_KEYS[i + 1]; var secret = member_value(statum, secret_key); var public_value = member_value(statum, public_key); if (secret.length > 0 && public_value.length > 0) { continue; } if (secret.length > 0 || public_value.length > 0) { repaired_pairs += secret_key + " + " + public_key; } else { added += secret_key; added += public_key; } statum.set_string_member(secret_key, generated_value(generated, secret_key)); statum.set_string_member(public_key, generated_value(generated, public_key)); } repaired = repaired_pairs; var node = new Json.Node(Json.NodeType.OBJECT); node.set_object(root); Generator.write_file(config_path, Json.to_string(node, true) + "\n", 0600); return added; } private static string member_value(Json.Object obj, string key) { if (!obj.has_member(key) || obj.get_string_member(key) == null) { return ""; } return ((!)obj.get_string_member(key)) ?? ""; } private static string generated_value(Json.Object generated, string key) throws Error { if (!generated.has_member(key) || generated.get_string_member(key) == null) { throw Generator.cli_error("statum-genkeys output lacks %s", key); } return (!)generated.get_string_member(key); } } }