Keys.vala 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. using GLib;
  2. using Json;
  3. namespace Spry.Cli {
  4. /**
  5. * `spry keys` — runs `statum-genkeys` and merges its key material into
  6. * the "statum" section of the application's `web-config.json`.
  7. *
  8. * Key material is maintained as PAIRS (a secret and its public half):
  9. * when either half of a pair is missing the whole pair is regenerated,
  10. * so a half-deleted file is repaired with a matching pair rather than a
  11. * mismatched one. Any run that (re)writes key material prints a
  12. * prominent warning: regenerating a pair invalidates existing sessions
  13. * and sealed blobs. Complete pairs and every other configuration
  14. * section are left untouched.
  15. */
  16. public class Keys : GLib.Object {
  17. /**
  18. * The static key fields Statum reads from the "statum" section,
  19. * ordered secret/public pair-wise so missing halves can be repaired
  20. * by regenerating the whole pair.
  21. */
  22. public const string[] STATUM_KEYS = {
  23. "signing_secret_key",
  24. "signing_public_key",
  25. "encryption_signing_secret_key",
  26. "encryption_signing_public_key",
  27. "encryption_sealing_secret_key",
  28. "encryption_sealing_public_key"
  29. };
  30. /**
  31. * Fills any missing "statum" key pairs in `config_path`, creating
  32. * the file (and the section) when absent, and repairs half-present
  33. * pairs by regenerating them whole. Does nothing when every key is
  34. * already configured.
  35. */
  36. public static void run(string config_path, string? tools_dir, string? libdir) throws Error {
  37. if (has_all_keys(config_path)) {
  38. stdout.printf("All static keys are already configured in %s — nothing to do.\n", config_path);
  39. return;
  40. }
  41. var generated = spawn_genkeys(tools_dir, libdir);
  42. string[] repaired;
  43. var added = merge(config_path, generated, out repaired);
  44. foreach (var key in added) {
  45. stdout.printf("Added %s to the statum section of %s\n", key, config_path);
  46. }
  47. foreach (var pair in repaired) {
  48. stdout.printf("Repaired an incomplete pair in %s — %s were regenerated\n", config_path, pair);
  49. }
  50. stderr.printf("*** WARNING: new static key material was generated in %s.\n", config_path);
  51. stderr.printf("*** Existing sessions and sealed blobs are invalidated by this change.\n");
  52. }
  53. private static bool has_all_keys(string config_path) {
  54. if (!FileUtils.test(config_path, FileTest.EXISTS)) {
  55. return false;
  56. }
  57. try {
  58. var parser = new Parser();
  59. parser.load_from_file(config_path);
  60. var root = parser.get_root();
  61. if (root == null || root.get_object() == null || !((!)root).get_object().has_member("statum")) {
  62. return false;
  63. }
  64. var statum = ((!)root).get_object().get_object_member("statum");
  65. foreach (var key in STATUM_KEYS) {
  66. if (!statum.has_member(key) || statum.get_string_member(key) == null
  67. || ((!)statum.get_string_member(key)).length == 0) {
  68. return false;
  69. }
  70. }
  71. return true;
  72. } catch (Error e) {
  73. return false;
  74. }
  75. }
  76. private static Json.Object spawn_genkeys(string? tools_dir, string? libdir) throws Error {
  77. var tool = Tools.resolve("statum-genkeys", tools_dir);
  78. var launcher = new SubprocessLauncher(SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_PIPE);
  79. Tools.apply_env(launcher, libdir);
  80. var process = launcher.spawnv({ tool });
  81. string stdout_buf;
  82. string stderr_buf;
  83. process.communicate_utf8(null, null, out stdout_buf, out stderr_buf);
  84. if (!process.get_successful()) {
  85. throw Generator.cli_error("statum-genkeys failed: %s", (stderr_buf ?? "").strip());
  86. }
  87. var parser = new Parser();
  88. parser.load_from_data(stdout_buf ?? "");
  89. var root = parser.get_root();
  90. if (root == null || root.get_object() == null || !((!)root).get_object().has_member("statum")) {
  91. throw Generator.cli_error("unexpected statum-genkeys output");
  92. }
  93. return ((!)root).get_object().get_object_member("statum");
  94. }
  95. /**
  96. * Merges the generated material pair-wise: complete pairs are kept,
  97. * pairs with a missing half are regenerated whole (`repaired` lists
  98. * them as `"secret + public"` labels), entirely absent pairs are
  99. * added (`added` lists their keys). The file is rewritten with mode
  100. * 0600 because it carries key material.
  101. */
  102. private static string[] merge(string config_path, Json.Object generated, out string[] repaired) throws Error {
  103. repaired = new string[0];
  104. var root = new Json.Object();
  105. if (FileUtils.test(config_path, FileTest.EXISTS)) {
  106. var parser = new Parser();
  107. try {
  108. parser.load_from_file(config_path);
  109. } catch (Error e) {
  110. throw Generator.cli_error("%s is not valid JSON: %s", config_path, e.message);
  111. }
  112. var existing = parser.get_root();
  113. if (existing == null || existing.get_object() == null) {
  114. throw Generator.cli_error("%s has no JSON object at the top level", config_path);
  115. }
  116. root = ((!)existing).get_object();
  117. }
  118. if (!root.has_member("statum")) {
  119. root.set_object_member("statum", new Json.Object());
  120. }
  121. var statum = root.get_object_member("statum");
  122. var added = new string[0];
  123. var repaired_pairs = new string[0];
  124. for (int i = 0; i + 1 < STATUM_KEYS.length; i += 2) {
  125. var secret_key = STATUM_KEYS[i];
  126. var public_key = STATUM_KEYS[i + 1];
  127. var secret = member_value(statum, secret_key);
  128. var public_value = member_value(statum, public_key);
  129. if (secret.length > 0 && public_value.length > 0) {
  130. continue;
  131. }
  132. if (secret.length > 0 || public_value.length > 0) {
  133. repaired_pairs += secret_key + " + " + public_key;
  134. } else {
  135. added += secret_key;
  136. added += public_key;
  137. }
  138. statum.set_string_member(secret_key, generated_value(generated, secret_key));
  139. statum.set_string_member(public_key, generated_value(generated, public_key));
  140. }
  141. repaired = repaired_pairs;
  142. var node = new Json.Node(Json.NodeType.OBJECT);
  143. node.set_object(root);
  144. Generator.write_file(config_path, Json.to_string(node, true) + "\n", 0600);
  145. return added;
  146. }
  147. private static string member_value(Json.Object obj, string key) {
  148. if (!obj.has_member(key) || obj.get_string_member(key) == null) {
  149. return "";
  150. }
  151. return ((!)obj.get_string_member(key)) ?? "";
  152. }
  153. private static string generated_value(Json.Object generated, string key) throws Error {
  154. if (!generated.has_member(key) || generated.get_string_member(key) == null) {
  155. throw Generator.cli_error("statum-genkeys output lacks %s", key);
  156. }
  157. return (!)generated.get_string_member(key);
  158. }
  159. }
  160. }