UNPKG

@xec-sh/core

Version:

Universal shell execution engine

150 lines 5.94 kB
import { readFile } from 'node:fs/promises'; export class SSHKeyValidator { static async validatePrivateKey(key) { const issues = []; let keyContent; if (Buffer.isBuffer(key)) { keyContent = key.toString('utf8'); } else { keyContent = key; } if (!keyContent.trim()) { issues.push('SSH key is empty'); return { isValid: false, issues }; } const normalizedKey = keyContent.trim().replace(/\r\n/g, '\n'); const opensshRegex = /-----BEGIN OPENSSH PRIVATE KEY-----\s*\n[\s\S]+?\n-----END OPENSSH PRIVATE KEY-----/; if (opensshRegex.test(normalizedKey)) { return { isValid: true, keyType: 'OPENSSH', issues }; } const pemRegex = /-----BEGIN (.+) PRIVATE KEY-----\s*\n[\s\S]+?\n-----END \1 PRIVATE KEY-----/; const match = normalizedKey.match(pemRegex); if (!match) { issues.push('Invalid SSH private key format. Expected PEM or OpenSSH format'); return { isValid: false, issues }; } const keyType = match[1]; if (!keyType) { issues.push('Invalid key format: missing key type'); return { isValid: false, issues }; } const validKeyTypes = ['RSA', 'DSA', 'EC', 'ECDSA', 'ED25519']; if (!validKeyTypes.includes(keyType.toUpperCase())) { issues.push(`Unsupported key type: ${keyType}`); } if (normalizedKey.includes('ENCRYPTED')) { issues.push('Encrypted private keys require a passphrase'); } const keyLines = normalizedKey.split('\n'); const contentLines = keyLines.slice(1, -1).filter(line => line.trim()); if (contentLines.length < 3) { issues.push('Private key content appears to be too short'); } const base64Lines = contentLines.filter(line => !line.includes(':')); const keyData = base64Lines.join(''); const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/; if (keyData && !base64Regex.test(keyData)) { issues.push('Private key content is not properly Base64 encoded'); } return { isValid: issues.length === 0, keyType: keyType ? keyType.toUpperCase() : undefined, issues }; } static validatePublicKey(key) { const issues = []; if (!key.trim()) { issues.push('SSH public key is empty'); return { isValid: false, issues }; } const normalizedKey = key.trim().replace(/\s+/g, ' '); const opensshPubKeyRegex = /^(ssh-rsa|ssh-dss|ecdsa-sha2-nistp\d+|ssh-ed25519)\s+[A-Za-z0-9+/]+={0,2}(\s+.+)?$/; const match = normalizedKey.match(opensshPubKeyRegex); if (!match) { issues.push('Invalid SSH public key format. Expected OpenSSH format'); return { isValid: false, issues }; } const keyType = match[1]; if (!keyType) { issues.push('Invalid SSH public key: missing key type'); return { isValid: false, issues }; } const keyTypeMap = { 'ssh-rsa': 'RSA', 'ssh-dss': 'DSA', 'ecdsa-sha2-nistp256': 'ECDSA', 'ecdsa-sha2-nistp384': 'ECDSA', 'ecdsa-sha2-nistp521': 'ECDSA', 'ssh-ed25519': 'ED25519' }; return { isValid: true, keyType: keyTypeMap[keyType] || keyType.toUpperCase(), issues }; } static async validateKeyFile(keyPath, passphrase) { try { const keyContent = await readFile(keyPath, 'utf8'); const result = await this.validatePrivateKey(keyContent); if (passphrase && !keyContent.includes('ENCRYPTED')) { result.issues.push('Passphrase provided but key does not appear to be encrypted'); } return result; } catch (error) { return { isValid: false, issues: [`Failed to read key file: ${error instanceof Error ? error.message : String(error)}`] }; } } static async checkKeyFilePermissions(keyPath) { const issues = []; try { const { stat } = await import('node:fs/promises'); const stats = await stat(keyPath); const mode = stats.mode & 0o777; if (mode !== 0o600 && mode !== 0o400) { issues.push(`SSH key file has insecure permissions: ${mode.toString(8)}. Should be 600 or 400`); } return { isSecure: issues.length === 0, issues }; } catch (error) { return { isSecure: false, issues: [`Failed to check file permissions: ${error instanceof Error ? error.message : String(error)}`] }; } } static validateSSHOptions(options) { const issues = []; if (!options.host) { issues.push('SSH host is required'); } if (!options.username) { issues.push('SSH username is required'); } if (options.port !== undefined) { if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) { issues.push('SSH port must be a valid port number (1-65535)'); } } if (!options.privateKey && !options.password) { issues.push('Either privateKey or password must be provided for authentication'); } if (options.privateKey && options.password) { issues.push('Both privateKey and password provided. Only one authentication method should be used'); } return { isValid: issues.length === 0, issues }; } } //# sourceMappingURL=ssh-key-validator.js.map