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: "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, "Key names are domain names, e.g. xfer-key or secondary.example.com."), 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); } }