ssh-manager-pro
Version:
🔑 Professional SSH key manager with automatic clipboard integration, cross-platform support, and zero-configuration setup
349 lines (289 loc) • 10 kB
JavaScript
const fs = require('fs');
const path = require('path');
class Validator {
/**
* Validate SSH key name
*/
static validateKeyName(name) {
const errors = [];
if (!name || typeof name !== 'string') {
errors.push('Key name is required and must be a string');
return { valid: false, errors };
}
// Check length
if (name.length < 1 || name.length > 255) {
errors.push('Key name must be between 1 and 255 characters');
}
// Check for invalid characters
const invalidChars = /[<>:"/\\|?*\x00-\x1f]/;
if (invalidChars.test(name)) {
errors.push('Key name contains invalid characters');
}
// Check for reserved names
const reservedNames = ['con', 'prn', 'aux', 'nul', 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'];
if (reservedNames.includes(name.toLowerCase())) {
errors.push('Key name is a reserved system name');
}
// Check for dots at start/end
if (name.startsWith('.') || name.endsWith('.')) {
errors.push('Key name cannot start or end with a dot');
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Validate key type
*/
static validateKeyType(keyType, supportedTypes = ['rsa', 'ed25519', 'ecdsa']) {
const errors = [];
if (!keyType || typeof keyType !== 'string') {
errors.push('Key type is required and must be a string');
return { valid: false, errors };
}
if (!supportedTypes.includes(keyType.toLowerCase())) {
errors.push(`Unsupported key type: ${keyType}. Supported: ${supportedTypes.join(', ')}`);
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Validate key size
*/
static validateKeySize(keySize, keyType) {
const errors = [];
if (typeof keySize !== 'number') {
errors.push('Key size must be a number');
return { valid: false, errors };
}
const validSizes = {
rsa: [2048, 3072, 4096],
ecdsa: [256, 384, 521],
ed25519: [256] // Fixed size, but we accept it
};
if (keyType && validSizes[keyType]) {
if (!validSizes[keyType].includes(keySize)) {
errors.push(`Invalid key size for ${keyType}: ${keySize}. Valid sizes: ${validSizes[keyType].join(', ')}`);
}
} else if (keySize < 1024 || keySize > 8192) {
errors.push('Key size must be between 1024 and 8192 bits');
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Validate comment
*/
static validateComment(comment) {
const errors = [];
if (comment !== undefined && comment !== null) {
if (typeof comment !== 'string') {
errors.push('Comment must be a string');
} else if (comment.length > 1024) {
errors.push('Comment must be less than 1024 characters');
}
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Validate passphrase
*/
static validatePassphrase(passphrase, options = {}) {
const errors = [];
const {
minLength = 0,
maxLength = 1024,
requireSpecialChars = false,
requireNumbers = false,
requireUppercase = false
} = options;
if (passphrase !== undefined && passphrase !== null) {
if (typeof passphrase !== 'string') {
errors.push('Passphrase must be a string');
return { valid: false, errors };
}
if (passphrase.length < minLength) {
errors.push(`Passphrase must be at least ${minLength} characters`);
}
if (passphrase.length > maxLength) {
errors.push(`Passphrase must be less than ${maxLength} characters`);
}
if (requireSpecialChars && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(passphrase)) {
errors.push('Passphrase must contain at least one special character');
}
if (requireNumbers && !/\d/.test(passphrase)) {
errors.push('Passphrase must contain at least one number');
}
if (requireUppercase && !/[A-Z]/.test(passphrase)) {
errors.push('Passphrase must contain at least one uppercase letter');
}
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Validate directory path
*/
static validateDirectory(dirPath) {
const errors = [];
if (!dirPath || typeof dirPath !== 'string') {
errors.push('Directory path is required and must be a string');
return { valid: false, errors };
}
// Expand tilde
const expandedPath = dirPath.startsWith('~')
? path.join(require('os').homedir(), dirPath.slice(2))
: dirPath;
try {
// Check if path is valid
path.parse(expandedPath);
// Check if directory exists or can be created
if (fs.existsSync(expandedPath)) {
const stats = fs.statSync(expandedPath);
if (!stats.isDirectory()) {
errors.push('Path exists but is not a directory');
}
} else {
// Check if parent directory exists
const parentDir = path.dirname(expandedPath);
if (!fs.existsSync(parentDir)) {
errors.push('Parent directory does not exist');
}
}
} catch (error) {
errors.push(`Invalid directory path: ${error.message}`);
}
return {
valid: errors.length === 0,
errors,
expandedPath
};
}
/**
* Validate SSH public key format
*/
static validateSSHPublicKey(publicKey) {
const errors = [];
if (!publicKey || typeof publicKey !== 'string') {
errors.push('Public key is required and must be a string');
return { valid: false, errors };
}
const trimmedKey = publicKey.trim();
// Check basic format
const sshKeyPatterns = [
/^ssh-rsa\s+[A-Za-z0-9+/]+=*(\s+.*)?$/,
/^ssh-ed25519\s+[A-Za-z0-9+/]+=*(\s+.*)?$/,
/^ecdsa-sha2-\w+\s+[A-Za-z0-9+/]+=*(\s+.*)?$/,
/^ssh-dss\s+[A-Za-z0-9+/]+=*(\s+.*)?$/
];
const isValidFormat = sshKeyPatterns.some(pattern => pattern.test(trimmedKey));
if (!isValidFormat) {
errors.push('Invalid SSH public key format');
}
// Extract key type first
let keyType = 'unknown';
if (trimmedKey.startsWith('ssh-rsa')) keyType = 'rsa';
else if (trimmedKey.startsWith('ssh-ed25519')) keyType = 'ed25519';
else if (trimmedKey.startsWith('ecdsa-sha2-')) keyType = 'ecdsa';
else if (trimmedKey.startsWith('ssh-dss')) keyType = 'dsa';
// Check length based on key type
const minLengths = {
'rsa': 200, // RSA keys are longer
'ed25519': 80, // ED25519 keys are shorter
'ecdsa': 100, // ECDSA keys vary
'dsa': 200, // DSA keys are longer
'unknown': 50 // Conservative minimum for unknown types
};
const maxLength = 8192;
const minLength = minLengths[keyType] || minLengths['unknown'];
if (trimmedKey.length < minLength) {
errors.push(`SSH public key appears to be too short for ${keyType.toUpperCase()} (minimum ${minLength} characters)`);
}
if (trimmedKey.length > maxLength) {
errors.push('SSH public key appears to be too long');
}
return {
valid: errors.length === 0,
errors,
keyType,
length: trimmedKey.length
};
}
/**
* Validate all key generation options
*/
static validateKeyOptions(options) {
const allErrors = [];
// Validate key name
if (options.keyName) {
const nameValidation = this.validateKeyName(options.keyName);
if (!nameValidation.valid) {
allErrors.push(...nameValidation.errors.map(e => `Key name: ${e}`));
}
}
// Validate key type
if (options.keyType) {
const typeValidation = this.validateKeyType(options.keyType);
if (!typeValidation.valid) {
allErrors.push(...typeValidation.errors.map(e => `Key type: ${e}`));
}
}
// Validate key size
if (options.keySize !== undefined) {
const sizeValidation = this.validateKeySize(options.keySize, options.keyType);
if (!sizeValidation.valid) {
allErrors.push(...sizeValidation.errors.map(e => `Key size: ${e}`));
}
}
// Validate comment
if (options.comment !== undefined) {
const commentValidation = this.validateComment(options.comment);
if (!commentValidation.valid) {
allErrors.push(...commentValidation.errors.map(e => `Comment: ${e}`));
}
}
// Validate passphrase
if (options.passphrase !== undefined) {
const passphraseValidation = this.validatePassphrase(options.passphrase);
if (!passphraseValidation.valid) {
allErrors.push(...passphraseValidation.errors.map(e => `Passphrase: ${e}`));
}
}
return {
valid: allErrors.length === 0,
errors: allErrors
};
}
/**
* Sanitize key name
*/
static sanitizeKeyName(name) {
if (!name || typeof name !== 'string') {
return 'id_rsa';
}
// Remove invalid characters
let sanitized = name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');
// Remove leading/trailing dots and spaces
sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, '');
// Ensure not empty
if (!sanitized) {
sanitized = 'id_rsa';
}
// Ensure not too long
if (sanitized.length > 255) {
sanitized = sanitized.substring(0, 255);
}
return sanitized;
}
}
module.exports = Validator;