@xec-sh/core
Version:
Universal shell execution engine
150 lines • 5.94 kB
JavaScript
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