import * as plugins from './smartssh.plugins.js'; import { SshKey } from './smartssh.classes.sshkey.js'; const unsafeHostMessage = 'SSH host aliases must be single, filename-safe values without whitespace or path separators.'; export const assertSafeHost = (hostArg: string) => { if (!hostArg || hostArg === '.' || hostArg === '..') { throw new Error(unsafeHostMessage); } if (!/^[A-Za-z0-9._@:%+-]+$/.test(hostArg)) { throw new Error(unsafeHostMessage); } }; export const isSafeHost = (hostArg: string) => { try { assertSafeHost(hostArg); return true; } catch { return false; } }; export const resolveSshDirPath = (dirPathArg?: string) => { return plugins.path.resolve( dirPathArg ?? plugins.path.join(plugins.smartpath.get.home(), '.ssh') ); }; export const ensureSshDirSync = (dirPathArg: string) => { plugins.fs.ensureDirSync(dirPathArg); plugins.fs.chmodSync(dirPathArg, 0o700); }; export const quoteSshConfigValue = (valueArg: string) => { if (/^[A-Za-z0-9._~/:@%+-]+$/.test(valueArg)) { return valueArg; } return `"${valueArg.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; }; export const fingerprintSha256 = (keyArg: Buffer | string) => { const hash = plugins.crypto .createHash('sha256') .update(keyArg) .digest('base64') .replace(/=+$/, ''); return `SHA256:${hash}`; }; export const fingerprintMd5 = (keyArg: Buffer | string) => { const hash = plugins.crypto.createHash('md5').update(keyArg).digest('hex'); return hash.match(/.{2}/g)?.join(':') ?? hash; }; export const sshKeyArrayFromDir = (dirArg: string): SshKey[] => { const resolvedDir = resolveSshDirPath(dirArg); if (!plugins.fs.pathExistsSync(resolvedDir)) { return []; } const entries = plugins.fs.readdirSync(resolvedDir, { withFileTypes: true }); const keyMap = new Map(); for (const entry of entries) { if (!entry.isFile()) { continue; } const fileName = entry.name; if ( fileName === 'config' || fileName === 'authorized_keys' || fileName === 'known_hosts' || fileName.startsWith('.') ) { continue; } const isPublicKey = fileName.endsWith('.pub'); const host = isPublicKey ? fileName.slice(0, -4) : fileName; if (!isSafeHost(host)) { continue; } const keyRecord = keyMap.get(host) ?? {}; const filePath = plugins.path.join(resolvedDir, fileName); const keyContent = plugins.fs.readFileSync(filePath, 'utf8'); if (isPublicKey) { keyRecord.public = keyContent; } else { keyRecord.private = keyContent; } keyMap.set(host, keyRecord); } return Array.from(keyMap.entries()).map(([host, keyRecord]) => { return new SshKey({ host, private: keyRecord.private, public: keyRecord.public, }); }); };