UNPKG

pure-js-sftp

Version:

A pure JavaScript SFTP client with revolutionary RSA-SHA2 compatibility fixes. Zero native dependencies, built on ssh2-streams with 100% SSH key support.

401 lines (391 loc) 17.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.enablePureJSSigningFix = enablePureJSSigningFix; exports.disablePureJSSigningFix = disablePureJSSigningFix; exports.isPureJSSigningFixEnabled = isPureJSSigningFixEnabled; const ssh2Streams = __importStar(require("ssh2-streams")); // Hash algorithm functions are defined inline in child process scripts let signingFixEnabled = false; const originalParseKey = ssh2Streams.utils.parseKey; function detectKeyTypeFromPem(pemContent) { // Detect key type from PEM content if (pemContent.includes('BEGIN RSA PRIVATE KEY') || pemContent.includes('ssh-rsa')) { return 'ssh-rsa'; } if (pemContent.includes('BEGIN DSA PRIVATE KEY') || pemContent.includes('ssh-dss')) { return 'ssh-dss'; } if (pemContent.includes('BEGIN EC PRIVATE KEY') || pemContent.includes('ecdsa-sha2-')) { // Try to detect ECDSA curve from content if (pemContent.includes('nistp256') || pemContent.includes('prime256v1')) { return 'ecdsa-sha2-nistp256'; } if (pemContent.includes('nistp384') || pemContent.includes('secp384r1')) { return 'ecdsa-sha2-nistp384'; } if (pemContent.includes('nistp521') || pemContent.includes('secp521r1')) { return 'ecdsa-sha2-nistp521'; } return 'ecdsa-sha2-nistp256'; // Default to P-256 } if (pemContent.includes('ssh-ed25519') || pemContent.includes('Ed25519')) { return 'ssh-ed25519'; } // Default to RSA if unknown return 'ssh-rsa'; } function detectKeyTypeFromOriginal(keyData) { // Try to detect key type from original key data (OpenSSH format, PPK, etc.) const keyStr = Buffer.isBuffer(keyData) ? keyData.toString() : keyData; // OpenSSH format detection - parse binary format properly if (keyStr.includes('BEGIN OPENSSH PRIVATE KEY')) { try { const lines = keyStr.split('\n'); const base64Data = lines .filter(line => !line.startsWith('-----')) .join('') .replace(/\s/g, ''); const keyBuffer = Buffer.from(base64Data, 'base64'); let offset = 0; // Skip magic bytes "openssh-key-v1\0" offset += 15; // Read cipher name const cipherNameLength = keyBuffer.readUInt32BE(offset); offset += 4; offset += cipherNameLength; // Skip cipher name // Read KDF name const kdfNameLength = keyBuffer.readUInt32BE(offset); offset += 4; offset += kdfNameLength; // Skip KDF name // Read KDF options const kdfOptionsLength = keyBuffer.readUInt32BE(offset); offset += 4; offset += kdfOptionsLength; // Skip KDF options // Read number of keys const numberOfKeys = keyBuffer.readUInt32BE(offset); offset += 4; // Read public key section const publicKeyLength = keyBuffer.readUInt32BE(offset); offset += 4; const publicKeyData = keyBuffer.subarray(offset, offset + publicKeyLength); // Parse the public key to determine type let pubOffset = 0; const keyTypeLength = publicKeyData.readUInt32BE(pubOffset); pubOffset += 4; const keyTypeName = publicKeyData.subarray(pubOffset, pubOffset + keyTypeLength).toString(); return keyTypeName; // Return the actual key type from the binary data } catch (error) { // Fallback to string search if binary parsing fails if (keyStr.includes('ssh-rsa')) return 'ssh-rsa'; if (keyStr.includes('ssh-dss')) return 'ssh-dss'; if (keyStr.includes('ecdsa-sha2-nistp256')) return 'ecdsa-sha2-nistp256'; if (keyStr.includes('ecdsa-sha2-nistp384')) return 'ecdsa-sha2-nistp384'; if (keyStr.includes('ecdsa-sha2-nistp521')) return 'ecdsa-sha2-nistp521'; if (keyStr.includes('ssh-ed25519')) return 'ssh-ed25519'; } } // PPK (PuTTY) format detection if (keyStr.includes('PuTTY-User-Key-File-')) { if (keyStr.includes('ssh-rsa')) return 'ssh-rsa'; if (keyStr.includes('ssh-dss')) return 'ssh-dss'; if (keyStr.includes('ecdsa-sha2-nistp256')) return 'ecdsa-sha2-nistp256'; if (keyStr.includes('ecdsa-sha2-nistp384')) return 'ecdsa-sha2-nistp384'; if (keyStr.includes('ecdsa-sha2-nistp521')) return 'ecdsa-sha2-nistp521'; if (keyStr.includes('ssh-ed25519')) return 'ssh-ed25519'; } // Traditional PEM format detection (fallback) if (keyStr.includes('BEGIN RSA PRIVATE KEY')) return 'ssh-rsa'; if (keyStr.includes('BEGIN DSA PRIVATE KEY')) return 'ssh-dss'; if (keyStr.includes('BEGIN EC PRIVATE KEY')) return 'ecdsa-sha2-nistp256'; // Default curve return null; // Unknown format } // Moved to shared utility: getHashAlgorithmForKeyType function signWithPureJSCrypto(traditionalPem, passphrase, data, keyType) { try { // Use child process approach for VSCode compatibility return signWithSystemCrypto(traditionalPem, passphrase, data, keyType); } catch (error) { throw new Error(`Pure JS crypto signing failed: ${error instanceof Error ? error.message : String(error)}`); } } function signWithSystemCrypto(traditionalPem, passphrase, data, keyType) { const fs = require('fs'); const { execSync } = require('child_process'); const tempFilename = '/tmp/ssh_sign_' + Math.random().toString(36).substring(7) + '.js'; const signingScript = ` const crypto = require('crypto'); try { const input = JSON.parse(process.argv[2]); const { privateKey, passphrase, data, keyType } = input; // Determine hash algorithm based on SSH key type function getHashAlgorithmForKeyType(keyType) { if (!keyType) return 'SHA1'; // Default for unknown types switch(keyType.toLowerCase()) { case 'ssh-rsa': case 'ssh-dss': return 'SHA1'; case 'ecdsa-sha2-nistp256': return 'SHA256'; case 'ecdsa-sha2-nistp384': return 'SHA384'; case 'ecdsa-sha2-nistp521': return 'SHA512'; case 'ssh-ed25519': return null; // Ed25519 uses direct signing default: return 'SHA1'; // Fallback to RSA default } } const hashAlgorithm = getHashAlgorithmForKeyType(keyType); let signature; // Determine if this is an OpenSSH format key const isOpenSSH = privateKey.includes('BEGIN OPENSSH PRIVATE KEY'); let keyOptions = { key: privateKey, passphrase: passphrase || undefined }; // For OpenSSH format keys, use ssh2-streams to convert to traditional PEM first if (isOpenSSH) { try { const ssh2Streams = require('ssh2-streams'); const ssh2Key = ssh2Streams.utils.parseKey(privateKey, passphrase); // If ssh2-streams can parse it, try to get traditional PEM if (ssh2Key && !ssh2Key instanceof Error) { // Look for PEM symbol in ssh2-streams key const privateKeyPemSymbol = Object.getOwnPropertySymbols(ssh2Key).find(s => s.toString().includes('Private key PEM') ); if (privateKeyPemSymbol && ssh2Key[privateKeyPemSymbol]) { // Use the traditional PEM format keyOptions = { key: ssh2Key[privateKeyPemSymbol], passphrase: passphrase || undefined }; } else { // ssh2-streams couldn't extract PEM, try Node.js crypto directly keyOptions = { key: privateKey, passphrase: passphrase || undefined }; } } else { // ssh2-streams failed, try Node.js crypto directly with OpenSSH format keyOptions = { key: privateKey, passphrase: passphrase || undefined }; } } catch (ssh2Error) { // ssh2-streams failed, try Node.js crypto directly keyOptions = { key: privateKey, passphrase: passphrase || undefined }; } } if (hashAlgorithm === null) { // Ed25519 keys use direct signing without hash algorithm signature = crypto.sign(null, Buffer.from(data, 'base64'), keyOptions); } else { // Traditional signing with hash algorithm const sign = crypto.createSign(hashAlgorithm); sign.update(Buffer.from(data, 'base64')); signature = sign.sign(keyOptions); } process.stdout.write(signature.toString('base64')); } catch (error) { process.stderr.write('SIGNING_ERROR: ' + error.message); process.exit(1); }`; try { fs.writeFileSync(tempFilename, signingScript); const input = JSON.stringify({ privateKey: traditionalPem, passphrase: passphrase || undefined, data: data.toString('base64'), keyType: keyType }); const result = execSync(`node ${tempFilename} ${JSON.stringify(input)}`, { encoding: 'utf8', stdio: 'pipe' }); fs.unlinkSync(tempFilename); return Buffer.from(result.trim(), 'base64'); } catch (error) { try { fs.unlinkSync(tempFilename); } catch { } throw error; } } function enablePureJSSigningFix() { if (signingFixEnabled) { return; // Already enabled } signingFixEnabled = true; // Monkey patch ssh2-streams parseKey function ssh2Streams.utils.parseKey = function (keyData, passphrase) { if (!signingFixEnabled || !keyData) { return originalParseKey.call(this, keyData, passphrase); } const originalResult = originalParseKey.call(this, keyData, passphrase); // If ssh2-streams failed to parse an OpenSSH key, try to create our own key object with child process signing if (!originalResult || originalResult instanceof Error) { const keyStr = Buffer.isBuffer(keyData) ? keyData.toString() : keyData; // Check if this is an OpenSSH format key that we can handle with child process signing if (keyStr.includes('BEGIN OPENSSH PRIVATE KEY')) { const keyType = detectKeyTypeFromOriginal(keyData); if (keyType) { // Create a minimal key object with child process signing const fallbackKey = { type: keyType, comment: '', sign: function (data) { try { return signWithSystemCrypto(keyStr, passphrase, data, keyType); } catch (error) { throw new Error(`Fallback OpenSSH signing failed for ${keyType}: ${error instanceof Error ? error.message : String(error)}`); } } }; return fallbackKey; } } // If we can't handle it, return the original result (which might be an Error) return originalResult; } const keysToProcess = Array.isArray(originalResult) ? originalResult : [originalResult]; const processedKeys = keysToProcess.map((key) => { if (!key || typeof key.sign !== 'function') { return key; } // Try to find key material from ssh2-streams Symbol properties const privateKeyPemSymbol = Object.getOwnPropertySymbols(key).find(s => s.toString().includes('Private key PEM')); // If no traditional PEM found, use child process signing for OpenSSH keys if (!privateKeyPemSymbol) { // Check if this is an OpenSSH format key that needs child process signing const keyStr = Buffer.isBuffer(keyData) ? keyData.toString() : keyData; if (keyStr.includes('BEGIN OPENSSH PRIVATE KEY')) { // This is an OpenSSH key - use child process signing directly with the original key const keyType = detectKeyTypeFromOriginal(keyData) || 'unknown'; // Replace sign method with child process signing key.sign = function (data) { try { return signWithSystemCrypto(keyStr, passphrase, data, keyType); } catch (error) { throw new Error(`Child process OpenSSH signing failed for ${keyType}: ${error instanceof Error ? error.message : String(error)}`); } }; return key; } // Try to get key type from ssh2-streams key object const keyType = key.type || key.fulltype || detectKeyTypeFromOriginal(keyData); if (!keyType || !key.sign) { return key; // Skip fix if we can't determine key type } // For non-PEM keys, use original ssh2-streams signing with better error handling const originalSign = key.sign; key.sign = function (data) { try { const result = originalSign.call(this, data); if (!result || result instanceof Error) { throw new Error('Original ssh2-streams signing failed'); } return result; } catch (error) { throw new Error(`SSH2-streams signing failed for ${keyType}: ${error instanceof Error ? error.message : String(error)}`); } }; return key; } const traditionalKeyPem = key[privateKeyPemSymbol]; if (!traditionalKeyPem || typeof traditionalKeyPem !== 'string') { return key; // Skip if PEM content is invalid } // Detect key type from the key object and PEM content const keyType = key.type || key.fulltype || detectKeyTypeFromPem(traditionalKeyPem); // Replace sign method with pure JS crypto implementation const originalSign = key.sign; key.sign = function (data) { try { return signWithPureJSCrypto(traditionalKeyPem, passphrase, data, keyType); } catch (error) { // Fallback to original signing if pure JS crypto fails try { return originalSign.call(this, data); } catch (fallbackError) { throw new Error(`Both pure JS and original signing failed. Pure JS: ${error instanceof Error ? error.message : String(error)}. Original: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`); } } }; return key; }); return Array.isArray(originalResult) ? processedKeys : processedKeys[0]; }; } function disablePureJSSigningFix() { if (!signingFixEnabled) { return; // Already disabled } signingFixEnabled = false; // Restore original parseKey function ssh2Streams.utils.parseKey = originalParseKey; } function isPureJSSigningFixEnabled() { return signingFixEnabled; } //# sourceMappingURL=pure-js-signing-fix.js.map