/**
* Scan a local `~/.ssh/` directory for candidate SSH public keys.
*
* Returns an array of { publicKey, fingerprint, type, path } records,
* one per .pub file we can read and parse. Silently skips files that
* don't parse (unknown key type, bad format) — those aren't errors,
* just non-candidates.
*
* This module never reads secret key files. It only looks at *.pub.
*
* Used by ssh-auto-import.js to seed a hyper-identity registry with
* local SSH keys on the daemon's first boot.
*/
import process from '#node:process'
import { readdir, readFile, stat } from '#node:fs/promises'
import { join } from '#node:path'
import { SSHKeyParser } from './ssh-parser.js'
/**
* Scan a directory for readable `*.pub` SSH public keys.
*
* @param {string} dir - directory to scan (typically `$HOME/.ssh`)
* @returns {Promise<Array<{ publicKey, fingerprint, type, path, comment }>>}
*/
export async function scanSSHKeys (dir) {
let entries
try {
entries = await readdir(dir)
} catch {
// $HOME/.ssh doesn't exist, or we can't read it — return empty (not an error)
return []
}
const out = []
for (const name of entries) {
if (!name.endsWith('.pub')) continue
const path = join(dir, name)
// Skip anything that isn't a regular file (symlink to revoked?
// directory with .pub suffix?)
let st
try {
st = await stat(path)
} catch {
continue
}
if (!st.isFile()) continue
let text
try {
text = await readFile(path, 'utf8')
} catch {
continue // unreadable — likely permissions issue, skip
}
const first = text.split('\n').find(line => line.trim().length > 0)
if (!first) continue
let parsed
try {
parsed = SSHKeyParser.parse(first)
} catch {
continue // unsupported key type (dsa, ecdsa, sk-*) — not fatal
}
let fingerprint
try {
fingerprint = SSHKeyParser.fingerprint(first)
} catch {
continue
}
// Extract the trailing comment (OpenSSH convention: "ssh-ed25519 AAA... user@host")
const parts = first.trim().split(/\s+/)
const comment = parts.slice(2).join(' ') || null
out.push({
publicKey: first.trim(),
fingerprint,
type: parsed.type,
path,
comment
})
}
return out
}
/**
* Scan the current process owner's SSH directory. Skips if the process
* has no $HOME (container without a passwd entry), or if we're running
* as root (safety — don't auto-import root's keys, that's usually a
* misconfig and the blast radius is large).
*
* @param {Object} [opts]
* @param {boolean} [opts.allowRoot=false] - override the root safety check
* @returns {Promise<Array<{ publicKey, fingerprint, type, path, comment }>>}
*/
export async function scanHomeSSHKeys (opts = {}) {
if (!process.env.HOME) return []
if (!opts.allowRoot && typeof process.getuid === 'function' && process.getuid() === 0) {
return []
}
return scanSSHKeys(join(process.env.HOME, '.ssh'))
}