Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 40 additions & 9 deletions bin/hash-password.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
#!/usr/bin/env node
import crypto from "node:crypto";
import readline from "node:readline";

const username = process.argv[2];
if (!username || !/^[a-zA-Z0-9._-]{1,64}$/.test(username)) {
console.error("usage: bin/hash-password.mjs USERNAME");
process.exit(2);
}
const terminal = readline.createInterface({ input: process.stdin, output: process.stderr, terminal: true });
terminal.question("Password: ", (password) => {
terminal.close();
if (password.length < 12) { console.error("password must be at least 12 characters"); process.exit(2); }
const salt = crypto.randomBytes(16);
const hash = crypto.scryptSync(password, salt, 64, { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
console.log(JSON.stringify({ username, passwordHash: `scrypt$16384$8$1$${salt.toString("base64url")}$${hash.toString("base64url")}` }));
});

// Echoing the password would leave it in scrollback, shell history, and any
// captured terminal log, so the prompt reads raw keystrokes instead.
function readPassword(prompt) {
return new Promise((resolve, reject) => {
const input = process.stdin;
if (!input.isTTY) {
reject(new Error("stdin must be a terminal"));
return;
}
process.stderr.write(prompt);
input.setRawMode(true);
input.resume();
input.setEncoding("utf8");
let value = "";
const settle = (error, result) => {
input.removeListener("data", onData);
input.setRawMode(false);
input.pause();
process.stderr.write("\n");
if (error) reject(error);
else resolve(result);
};
const onData = (chunk) => {
for (const character of chunk) {
if (character === "\r" || character === "\n" || character === "\u0004") return settle(null, value);
if (character === "\u0003") return settle(new Error("cancelled"));
if (character === "\u007f" || character === "\b") value = value.slice(0, -1);
else if (character >= " ") value += character;
}
};
input.on("data", onData);
});
}

const password = await readPassword("Password: ");
if (password.length < 12) { console.error("password must be at least 12 characters"); process.exit(2); }
const salt = crypto.randomBytes(16);
const hash = crypto.scryptSync(password, salt, 64, { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
console.log(JSON.stringify({ username, passwordHash: `scrypt$16384$8$1$${salt.toString("base64url")}$${hash.toString("base64url")}` }));
1 change: 1 addition & 0 deletions control-server/public/styles.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
:root { --ink:#171811; --paper:#e9e3d2; --acid:#d8ff3e; --rust:#bb3e24; --muted:#817d70; --line:#3a3b31; }
* { box-sizing:border-box; }
[hidden] { display:none !important; }
body { margin:0; min-height:100vh; background:var(--ink); color:var(--paper); font-family:"IBM Plex Mono","Courier New",monospace; }
.grain { position:fixed; inset:0; opacity:.12; pointer-events:none; background-image:repeating-linear-gradient(0deg,transparent,transparent 3px,#fff 4px); mix-blend-mode:overlay; }
.eyebrow { margin:0 0 .5rem; color:var(--acid); font-size:.72rem; letter-spacing:.2em; text-transform:uppercase; }
Expand Down