import { h, mount, table, field, input, select, toast, errorBox, confirmDialog, modal, copyBox } from "../ui.js"; import { get, change, describeResult } from "../api.js"; import { rerender } from "../app.js"; const algorithms = ["hmac-sha256", "hmac-sha512", "hmac-sha384", "hmac-sha224", "hmac-sha1", "hmac-md5"]; // randomSecret returns a base64 secret as long as the algorithm's hash. function randomSecret(alg) { const bytes = { "hmac-sha512": 64, "hmac-sha384": 48, "hmac-sha224": 28, "hmac-sha1": 20, "hmac-md5": 16 }[alg] || 32; const raw = new Uint8Array(bytes); crypto.getRandomValues(raw); return btoa(String.fromCharCode(...raw)); } export async function render(el) { const keys = await get("/tsig-keys"); mount(el, h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "TSIG keys"), h("div", { class: "muted" }, "Shared secrets that authenticate zone transfers, NOTIFY and other server-to-server traffic. Use them in ACLs and options as 'key NAME'."), h("div", { class: "muted small" }, "To let a script, tool or person change only some zones or records through Jarl, create an ", h("a", { href: "#/account" }, "API token"), " restricted to them instead.")), h("div", { class: "actions" }, h("button", { class: "primary", onclick: () => keyDialog() }, "New key"))), h("div", { class: "panel" }, table([ { label: "Name", render: (k) => h("strong", { class: "mono" }, k.name) }, { label: "Algorithm", render: (k) => k.algorithm, tdClass: "mono" }, { label: "Used by", render: (k) => k.used_by.length ? k.used_by.join(", ") : h("span", { class: "muted" }, "unused"), tdClass: "small" }, { label: "Comment", render: (k) => h("span", { class: "muted" }, k.comment || "") }, { label: "", render: (k) => h("span", null, h("button", { class: "small", onclick: () => reveal(k) }, "Show secret"), " ", h("button", { class: "small", onclick: () => rotate(k) }, "New secret"), " ", h("button", { class: "small danger", disabled: k.used_by.length > 0, onclick: () => del(k) }, "Delete")), tdClass: "actions-cell" }, ], keys, { empty: "No TSIG keys. The rndc control key normally stays in /etc/bind/rndc.key and is not managed here." }))); } function snippet(k) { return 'key "' + k.name + '" {\n\talgorithm ' + k.algorithm + ';\n\tsecret "' + k.secret + '";\n};'; } function showSecret(k, title) { return modal(title, h("div", null, h("p", null, "Secret of ", h("strong", { class: "mono" }, k.name), ":"), copyBox(k.secret), h("h3", null, "named.conf for the other server"), h("pre", null, snippet(k)), h("h3", null, "Usage"), h("pre", null, "dig @this-server example.com AXFR -y " + k.algorithm + ":" + k.name + ":" + k.secret))); } async function keyDialog() { const err = h("div"); const name = input({ placeholder: "xfer-key", class: "mono" }); const alg = select(algorithms, "hmac-sha256"); const secret = input({ placeholder: "leave empty to generate a random secret", class: "mono" }); const comment = input(); await modal("New TSIG key", h("div", null, h("div", { class: "form-grid" }, field("Name", name, "Letters, digits, hyphens and dots, e.g. xfer-key or ns2-xfer.adbraun.lol. It's only a label (it doesn't have to exist in DNS), but both servers must use exactly the same name."), field("Algorithm", alg), field("Secret", secret, "Base64. Must be the same on both servers."), field("Comment", comment)), err), [ { label: "Cancel", value: null }, { label: "Create key", kind: "primary", onclick: async () => { const data = { name: name.value.trim(), algorithm: alg.value, comment: comment.value, secret: secret.value.trim() || randomSecret(alg.value) }; try { const r = await change("POST", "/tsig-keys", data); toast(describeResult(r)); await showSecret({ name: data.name, algorithm: data.algorithm, secret: data.secret }, r.staged ? "Key staged" : "Key created"); rerender(); return true; } catch (e) { mount(err, errorBox(e)); return false; } } }, ], { sticky: true }); } async function reveal(k) { try { const full = await get("/tsig-keys/" + k.id + "?secret=1"); await showSecret(full, "TSIG key " + k.name); } catch (e) { toast(e.message, "err", 8000); } } async function rotate(k) { if (!(await confirmDialog("New secret", "Generate a new secret for " + k.name + "? Every server using the key must be updated with the new secret.", "Generate", "danger"))) return; const secret = randomSecret(k.algorithm); try { const r = await change("PATCH", "/tsig-keys/" + k.id, { secret }); toast(describeResult(r)); await showSecret({ ...k, secret }, "New secret for " + k.name); rerender(); } catch (e) { toast(e.message, "err", 8000); } } async function del(k) { if (!(await confirmDialog("Delete key", "Delete TSIG key " + k.name + "?", "Delete", "danger"))) return; try { const r = await change("DELETE", "/tsig-keys/" + k.id); toast(describeResult(r)); rerender(); } catch (e) { toast(e.message, "err", 8000); } }