import { createHash } from "node:crypto";
import { db } from "./db";
export type SshKey = {
id: number;
userId: number;
title: string;
keyType: string;
publicKey: string;
fingerprint: string;
createdAt: number;
lastUsedAt: number | null;
};
type KeyRow = {
id: number;
user_id: number;
title: string;
key_type: string;
public_key: string;
fingerprint: string;
created_at: number;
last_used_at: number | null;
};
const toKey = (r: KeyRow): SshKey => ({
id: r.id,
userId: r.user_id,
title: r.title,
keyType: r.key_type,
publicKey: r.public_key,
fingerprint: r.fingerprint,
createdAt: r.created_at,
lastUsedAt: r.last_used_at,
});
const ALLOWED_TYPES = new Set([
"ssh-ed25519",
"ssh-rsa",
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
"sk-ssh-ed25519@openssh.com",
"sk-ecdsa-sha2-nistp256@openssh.com",
]);
/** Same format OpenSSH prints: SHA256:<unpadded base64 of the key blob's digest>. */
export function fingerprintOf(blob: Buffer) {
return `SHA256:${createHash("sha256").update(blob).digest("base64").replace(/=+$/, "")}`;
}
export class KeyError extends Error {}
/** Parses an OpenSSH public key line ("type base64 [comment]"). */
export function parsePublicKey(line: string) {
const [type, data, ...comment] = line.trim().split(/\s+/);
if (!type || !data || !ALLOWED_TYPES.has(type)) {
throw new KeyError("Key is invalid. You must supply a key in OpenSSH public key format (ssh-ed25519, ssh-rsa or ecdsa).");
}
const blob = Buffer.from(data, "base64");
// The blob starts with a length-prefixed copy of the key type; it must match.
const len = blob.length >= 4 ? blob.readUInt32BE(0) : 0;
if (blob.length < 4 + len || blob.subarray(4, 4 + len).toString() !== type) {
throw new KeyError("Key is invalid. The key data does not match its type.");
}
if (type === "ssh-rsa" && blob.length < 270) throw new KeyError("RSA keys must be at least 2048 bits.");
return { type, blob, publicKey: `${type} ${data}`, comment: comment.join(" ") };
}
export function addSshKey(userId: number, title: string, keyText: string) {
const parsed = parsePublicKey(keyText);
const fingerprint = fingerprintOf(parsed.blob);
if (db.prepare("SELECT 1 FROM ssh_keys WHERE fingerprint = ?").get(fingerprint)) {
throw new KeyError("Key is already in use.");
}
db.prepare(
"INSERT INTO ssh_keys (user_id, title, key_type, public_key, fingerprint, created_at) VALUES (?, ?, ?, ?, ?, ?)",
).run(userId, (title.trim() || parsed.comment || parsed.type).slice(0, 100), parsed.type, parsed.publicKey, fingerprint, Date.now());
}
export function listSshKeys(userId: number) {
return (db.prepare("SELECT * FROM ssh_keys WHERE user_id = ? ORDER BY created_at DESC").all(userId) as KeyRow[]).map(toKey);
}
export function deleteSshKey(userId: number, keyId: number) {
db.prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ?").run(keyId, userId);
}
export function findKeyByBlob(blob: Buffer) {
const row = db.prepare("SELECT * FROM ssh_keys WHERE fingerprint = ?").get(fingerprintOf(blob)) as KeyRow | undefined;
return row ? toKey(row) : null;
}
export function touchSshKey(keyId: number) {
db.prepare("UPDATE ssh_keys SET last_used_at = ? WHERE id = ?").run(Date.now(), keyId);
}