Détail du package

ppk-to-openssh

cartpauj789GPL-3.03.2.0

A pure JavaScript library for parsing and converting PuTTY private key files (.ppk) to OpenSSH format. Supports all PPK versions (v2 and v3) and key types (RSA, DSA, ECDSA, Ed25519). Handles both encrypted and unencrypted keys with full MAC verification.

ppk, putty, openssh, ssh

readme

ppk-to-openssh

npm version License: GPL v3

A pure JavaScript library for parsing and converting PuTTY private key files (.ppk) to OpenSSH format. Supports all PPK versions (v2 and v3) and key types (RSA, DSA, ECDSA, Ed25519). Handles both encrypted and unencrypted keys with full MAC verification. Production-ready PPK v3 support with universal Argon2 implementation that works in Node.js, browsers, and any JavaScript environment. Comprehensively tested with 200+ test cases covering all PPK variants and edge cases.

✨ Features

  • Complete PPK Support: Handles PPK versions 2 and 3 with full feature parity
  • All Key Types: RSA, DSA, ECDSA (P-256, P-384, P-521), and Ed25519
  • Pure JavaScript Encryption: Encrypt output keys with pure JS for ALL key types (including Ed25519)
  • Dual Output Formats: Legacy PEM format (default) and modern OpenSSH format with full DSA support
  • Production-Ready PPK v3: Full Argon2id/Argon2i/Argon2d support with HMAC-SHA-256 verification
  • ES Modules Optimized: Full ES6 import/export support for optimal webpack bundling and tree shaking
  • Universal Argon2: WebAssembly-based implementation works in browsers and Node.js
  • Security: Full MAC verification, input validation, and cryptographic best practices
  • Minimal Dependencies: Only sshpk and hash-wasm for universal compatibility
  • Universal Compatibility: Works in Node.js, browsers, VS Code extensions, and any JavaScript environment
  • Cross-Platform: Linux, macOS, Windows support
  • TypeScript: Includes comprehensive TypeScript definitions
  • CLI Tool: Command-line interface for easy conversion
  • Comprehensive Error Handling: Detailed error codes and helpful hints
  • Extensive Testing: 200+ test cases covering all PPK variants, edge cases, and special passphrases

📦 Installation

For End Users

npm install ppk-to-openssh

The library uses hash-wasm for universal Argon2 support, ensuring PPK v3 compatibility across all JavaScript environments.

For Developers

If you want to contribute or build from source:

# Clone the repository
git clone https://github.com/cartpauj/ppk-to-openssh.git
cd ppk-to-openssh

# Install dependencies
npm install

# Build the library
npm run build

# Run tests
npm test

# Test the CLI
./bin/cli.js --help

Development Scripts

  • npm run build - Build both CommonJS and ES modules
  • npm run build:cjs - Build CommonJS version only
  • npm run build:esm - Build ES modules version only
  • npm run build:types - Copy TypeScript definitions
  • npm test - Run the test suite
  • npm run clean - Remove built files

🚀 Quick Start

Command Line Usage

# Convert a PPK file
npx ppk-to-openssh mykey.ppk

# With passphrase for encrypted keys
npx ppk-to-openssh mykey.ppk -p mypassphrase

# Interactive mode - prompts for passphrase if needed (input hidden)
npx ppk-to-openssh encrypted.ppk

# Specify output location
npx ppk-to-openssh mykey.ppk id_rsa --output ~/.ssh/

# Show help
npx ppk-to-openssh --help

Interactive Passphrase Prompting

The CLI automatically detects encrypted PPK files and prompts for passphrases when needed:

$ npx ppk-to-openssh encrypted.ppk
This PPK file is encrypted. Enter passphrase: [hidden input]
Conversion completed successfully

JavaScript API

📚 Usage Examples

1. Basic File Conversion

const { parseFromFile } = require('ppk-to-openssh');

async function convertPPK() {
  try {
    const result = await parseFromFile('./mykey.ppk', 'optional-passphrase');

    console.log('Private Key:');
    console.log(result.privateKey);

    console.log('Public Key:');
    console.log(result.publicKey);

    console.log('Fingerprint:', result.fingerprint);
    console.log('Algorithm:', result.algorithm);
    console.log('Comment:', result.comment);
  } catch (error) {
    console.error('Conversion failed:', error.message);
  }
}

2. Convert from String Content

const fs = require('fs');
const { parseFromString } = require('ppk-to-openssh');

async function convertFromString() {
  const ppkContent = fs.readFileSync('./mykey.ppk', 'utf8');
  const result = await parseFromString(ppkContent, 'passphrase');

  // Save the converted keys
  fs.writeFileSync('./id_rsa', result.privateKey);
  fs.writeFileSync('./id_rsa.pub', result.publicKey);
}

3. Convert with Output Encryption (NEW!)

const { parseFromString } = require('ppk-to-openssh');

async function convertWithEncryption() {
  const ppkContent = fs.readFileSync('./mykey.ppk', 'utf8');

  // Convert and encrypt the output private key
  const result = await parseFromString(ppkContent, 'input-passphrase', {
    encrypt: true,
    outputPassphrase: 'new-secure-password'
  });

  // Private key is now encrypted with 'new-secure-password'
  console.log('Encrypted private key:', result.privateKey.split('\n')[0]);
  // Output: -----BEGIN OPENSSH PRIVATE KEY----- (encrypted)

  fs.writeFileSync('./id_rsa', result.privateKey);
  fs.writeFileSync('./id_rsa.pub', result.publicKey);
}

// Works with ALL key types including Ed25519!
async function encryptEd25519() {
  const ppkContent = fs.readFileSync('./ed25519-key.ppk', 'utf8');

  const result = await parseFromString(ppkContent, '', {
    encrypt: true,
    outputPassphrase: 'secure-ed25519-password'
  });

  // Ed25519 key successfully encrypted with pure JavaScript!
  console.log('Ed25519 key encrypted successfully');
}

4. Batch Processing Multiple Keys

const { parseFromFile } = require('ppk-to-openssh');
const fs = require('fs');
const path = require('path');

async function convertMultipleKeys(directory, passphrase = '') {
  const files = fs.readdirSync(directory).filter(f => f.endsWith('.ppk'));

  for (const file of files) {
    try {
      const filePath = path.join(directory, file);
      const result = await parseFromFile(filePath, passphrase);

      const baseName = path.basename(file, '.ppk');
      fs.writeFileSync(`${baseName}`, result.privateKey);
      fs.writeFileSync(`${baseName}.pub`, result.publicKey);

      console.log(`✓ Converted ${file}`);
    } catch (error) {
      console.error(`✗ Failed to convert ${file}:`, error.message);
    }
  }
}

5. Using the PPKParser Class with Options

const { PPKParser } = require('ppk-to-openssh');

async function advancedUsage() {
  // Basic parser without encryption
  const parser = new PPKParser({
    maxFileSize: 2 * 1024 * 1024, // 2MB limit
    maxFieldSize: 1024 * 1024     // 1MB field limit
  });

  const ppkContent = fs.readFileSync('./mykey.ppk', 'utf8');
  const result = await parser.parse(ppkContent, 'passphrase'); // Only 2 parameters!

  console.log('Algorithm:', result.algorithm);
  if (result.curve) {
    console.log('Curve:', result.curve);
  }

  // Access parser info
  console.log('Supported algorithms:', parser.supportedAlgorithms);
}

// Example with output encryption - outputPassphrase goes in constructor
async function encryptedOutput() {
  const parser = new PPKParser({
    outputFormat: 'openssh',
    outputPassphrase: 'new-secure-password'  // Goes in constructor, not parse()
  });

  const ppkContent = fs.readFileSync('./mykey.ppk', 'utf8');
  const result = await parser.parse(ppkContent, 'input-passphrase'); // Still only 2 parameters!

  // Private key is now encrypted with 'new-secure-password'
  console.log('Encrypted private key:', result.privateKey.split('\n')[0]);
}

6. OpenSSH Format Output (ssh2-streams Compatible)

const { PPKParser } = require('ppk-to-openssh');

async function openSSHFormatExample() {
  // Create parser with OpenSSH output format
  const parser = new PPKParser({
    outputFormat: 'openssh'  // Use modern OpenSSH format instead of legacy PEM
  });

  const ppkContent = fs.readFileSync('./mykey.ppk', 'utf8');
  const result = await parser.parse(ppkContent, 'passphrase'); // Only 2 parameters

  // Private key will be in OpenSSH format for ssh2-streams compatibility
  console.log('Private Key Format:', result.privateKey.split('\n')[0]);
  // Output: -----BEGIN OPENSSH PRIVATE KEY-----

  // Use with ssh2-streams based libraries
  const { Client } = require('ssh2');
  const conn = new Client();

  conn.connect({
    host: 'example.com',
    username: 'user',
    privateKey: result.privateKey,  // OpenSSH format works perfectly
    passphrase: 'key-passphrase'    // If the converted key is encrypted
  });
}

// Example with encrypted output using PPKParser directly
async function encryptedOpenSSHOutput() {
  const parser = new PPKParser({
    outputFormat: 'openssh',
    outputPassphrase: 'secure-output-password'  // Encryption options go in constructor
  });

  const ppkContent = fs.readFileSync('./mykey.ppk', 'utf8');
  const result = await parser.parse(ppkContent, 'input-passphrase'); // Only 2 parameters

  // Private key is encrypted with 'secure-output-password'
  console.log('Encrypted OpenSSH key:', result.privateKey.split('\n')[0]);
}

// Backward compatibility: Default behavior unchanged
async function defaultBehavior() {
  const { parseFromFile } = require('ppk-to-openssh');

  // Still outputs PEM format by default (no breaking changes)
  const result = await parseFromFile('./mykey.ppk', 'passphrase');
  console.log(result.privateKey.split('\n')[0]);
  // Output: -----BEGIN RSA PRIVATE KEY----- (or DSA/EC for other key types)
}

7. ES Modules (ESM) Usage

import { parseFromFile, parseFromString, PPKError } from 'ppk-to-openssh';

// Basic usage
try {
  const result = await parseFromFile('./mykey.ppk');
  console.log('Conversion successful!');
} catch (error) {
  if (error instanceof PPKError) {
    console.error(`PPK Error [${error.code}]:`, error.message);
    if (error.details.hint) {
      console.error('Hint:', error.details.hint);
    }
  }
}

// With dynamic import
const ppkConverter = await import('ppk-to-openssh');
const result = await ppkConverter.parseFromFile('./mykey.ppk');

8. TypeScript Usage

import { parseFromFile, PPKParseResult, PPKError } from 'ppk-to-openssh';

async function convertKey(filePath: string, passphrase?: string): Promise<PPKParseResult> {
  try {
    const result: PPKParseResult = await parseFromFile(filePath, passphrase);
    return result;
  } catch (error) {
    if (error instanceof PPKError) {
      console.error(`Error [${error.code}]:`, error.message);
      throw error;
    }
    throw new Error(`Unexpected error: ${error}`);
  }
}

9. Browser Usage (with bundlers)

// In a browser environment with webpack/rollup/etc
// Universal Argon2 support via WebAssembly!
import { parseFromString } from 'ppk-to-openssh';

// Handle file upload
document.getElementById('fileInput').addEventListener('change', async (event) => {
  const file = event.target.files[0];
  if (file) {
    const content = await file.text();
    const passphrase = document.getElementById('passphrase').value;

    try {
      const result = await parseFromString(content, passphrase);
      document.getElementById('output').textContent = result.publicKey;
    } catch (error) {
      console.error('Conversion failed:', error.message);
    }
  }
});

10. VS Code Extension Usage

// Perfect for VS Code extensions - minimal dependencies!
const vscode = require('vscode');
const { parseFromString } = require('ppk-to-openssh');

async function convertPPKCommand() {
  try {
    // Get PPK content from user
    const ppkContent = await vscode.window.showInputBox({
      prompt: 'Paste your PPK file content',
      multiline: true
    });

    const passphrase = await vscode.window.showInputBox({
      prompt: 'Enter passphrase (leave empty for unencrypted keys)',
      password: true
    });

    const result = await parseFromString(ppkContent, passphrase || '');

    // Show result in new document
    const doc = await vscode.workspace.openTextDocument({
      content: result.privateKey,
      language: 'text'
    });
    await vscode.window.showTextDocument(doc);

    vscode.window.showInformationMessage('PPK converted successfully!');
  } catch (error) {
    vscode.window.showErrorMessage(`Conversion failed: ${error.message}`);
  }
}

11. Express.js API Endpoint

const express = require('express');
const { parseFromString, PPKError } = require('ppk-to-openssh');
const app = express();

app.use(express.json());

app.post('/convert-ppk', async (req, res) => {
  try {
    const { ppkContent, passphrase } = req.body;

    if (!ppkContent) {
      return res.status(400).json({ error: 'PPK content is required' });
    }

    const result = await parseFromString(ppkContent, passphrase || '');

    res.json({
      success: true,
      publicKey: result.publicKey,
      fingerprint: result.fingerprint,
      algorithm: result.algorithm
    });
  } catch (error) {
    if (error instanceof PPKError) {
      res.status(400).json({
        success: false,
        error: error.message,
        code: error.code,
        hint: error.details.hint
      });
    } else {
      res.status(500).json({
        success: false,
        error: 'Internal server error'
      });
    }
  }
});

12. Stream Processing

const { parseFromString } = require('ppk-to-openssh');
const fs = require('fs');
const { Transform } = require('stream');

class PPKConverter extends Transform {
  constructor(passphrase = '') {
    super({ objectMode: true });
    this.passphrase = passphrase;
  }

  async _transform(chunk, encoding, callback) {
    try {
      const result = await parseFromString(chunk.toString(), this.passphrase);
      this.push({
        filename: chunk.filename,
        publicKey: result.publicKey,
        privateKey: result.privateKey,
        fingerprint: result.fingerprint
      });
      callback();
    } catch (error) {
      callback(error);
    }
  }
}

// Usage
const converter = new PPKConverter('mypassphrase');
// ... pipe PPK content through converter

13. CLI Integration in Node.js Scripts

const { spawn } = require('child_process');
const { parseFromFile } = require('ppk-to-openssh');

async function convertAndUseSSH(ppkPath, host, command) {
  // Convert PPK to OpenSSH format
  const result = await parseFromFile(ppkPath, process.env.PPK_PASSPHRASE);

  // Write temporary key file
  const tmpKeyPath = '/tmp/ssh_key';
  fs.writeFileSync(tmpKeyPath, result.privateKey, { mode: 0o600 });

  try {
    // Use with SSH
    const ssh = spawn('ssh', ['-i', tmpKeyPath, host, command]);

    ssh.stdout.on('data', (data) => {
      console.log(data.toString());
    });

    ssh.stderr.on('data', (data) => {
      console.error(data.toString());
    });

  } finally {
    // Clean up temporary file
    fs.unlinkSync(tmpKeyPath);
  }
}

📖 API Reference

⚠️ Important API Notes

This library provides two different APIs with different parameter signatures:

1. Wrapper Functions (Recommended)

// These support options as 3rd parameter:
parseFromString(ppkContent, passphrase, options)
parseFromFile(filePath, passphrase, options)

// Example with encryption:
const result = await parseFromString(ppkContent, 'input-pass', {
  encrypt: true,
  outputPassphrase: 'output-pass'
});

2. PPKParser Class (Advanced)

// Constructor takes all options, parse() only takes 2 parameters:
const parser = new PPKParser({
  outputFormat: 'openssh',
  outputPassphrase: 'output-pass'  // Goes in constructor!
});

const result = await parser.parse(ppkContent, 'input-pass'); // Only 2 params!

Key Difference: Wrapper functions accept options in the function call, while PPKParser takes options in the constructor.

Functions

parseFromFile(filePath, passphrase?, options?)

Convert a PPK file from the filesystem.

  • filePath string - Path to the PPK file
  • passphrase string (optional) - Passphrase for encrypted keys
  • options object (optional) - Configuration options
    • encrypt boolean - Whether to encrypt the output private key
    • outputPassphrase string - Passphrase for encrypting the output (required if encrypt is true)
  • Returns Promise<PPKParseResult> - Conversion result
  • Throws PPKError - On parsing errors

parseFromString(ppkContent, passphrase?, options?)

Convert PPK content from a string.

  • ppkContent string - PPK file content
  • passphrase string (optional) - Passphrase for encrypted keys
  • options object (optional) - Configuration options
    • encrypt boolean - Whether to encrypt the output private key
    • outputPassphrase string - Passphrase for encrypting the output (required if encrypt is true)
  • Returns Promise<PPKParseResult> - Conversion result
  • Throws PPKError - On parsing errors

convertPPKWithEncryption(ppkContent, inputPassphrase?, outputPassphrase)

Convert PPK content and encrypt the output with pure JavaScript (supports ALL key types including Ed25519).

  • ppkContent string - PPK file content
  • inputPassphrase string (optional) - Passphrase for encrypted PPK files
  • outputPassphrase string - Passphrase to encrypt the output key with
  • Returns Promise<PPKParseResult> - Conversion result with encrypted private key
  • Throws Error - On conversion or encryption errors

Classes

PPKParser

Main parser class with configurable options.

const parser = new PPKParser({
  maxFileSize: 1024 * 1024,  // Maximum file size (default: 1MB)
  maxFieldSize: 1024 * 1024, // Maximum field size (default: 1MB)
  outputFormat: 'pem'        // Output format: 'pem' or 'openssh' (default: 'pem')
});

// Properties
parser.supportedAlgorithms  // Array of supported key algorithms
parser.maxFileSize         // Current max file size setting

// Methods
await parser.parse(ppkContent, passphrase)  // Parse PPK content (2 parameters only!)

Constructor Options:

  • maxFileSize (number): Maximum PPK file size in bytes (default: 1MB)
  • maxFieldSize (number): Maximum individual field size in bytes (default: 1MB)
  • outputFormat (string): Private key output format - 'pem' or 'openssh' (default: 'pem')
  • outputPassphrase (string): Passphrase to encrypt the output private key (when provided, automatically encrypts output)

PPKError

Custom error class with structured error information.

try {
  await parseFromFile('./key.ppk', 'wrong-pass');
} catch (error) {
  if (error instanceof PPKError) {
    console.log(error.name);       // 'PPKError'
    console.log(error.message);    // Human-readable error message
    console.log(error.code);       // Structured error code
    console.log(error.details);    // Additional context object
  }
}

Properties:

  • name (string): Always 'PPKError'
  • message (string): Human-readable error description
  • code (string): Structured error code for programmatic handling
  • details (object): Additional error context and hints

Common Error Codes:

  • INVALID_INPUT - Invalid input parameters
  • FILE_NOT_FOUND - PPK file not found
  • WRONG_FORMAT - Not a PPK file (OpenSSH/PEM detected)
  • INVALID_PPK_FORMAT - Missing PPK header
  • UNSUPPORTED_VERSION - Unsupported PPK version
  • PASSPHRASE_REQUIRED - Encrypted key needs passphrase
  • INVALID_MAC - Wrong passphrase or corrupted file
  • UNSUPPORTED_ALGORITHM - Unsupported key algorithm
  • FILE_TOO_LARGE - File exceeds size limit
  • UNSUPPORTED_ARGON2 - Unsupported Argon2 variant
  • UNSUPPORTED_ENCRYPTION - Unsupported encryption method

Types

PPKParseResult

interface PPKParseResult {
  privateKey: string;    // OpenSSH/PEM format private key
  publicKey: string;     // OpenSSH format public key  
  fingerprint: string;   // SHA256 fingerprint
  algorithm: string;     // Key algorithm (ssh-rsa, ssh-dss, ecdsa-sha2-*, ssh-ed25519)
  comment: string;       // Key comment from PPK file
  curve?: string;        // Curve name for ECDSA keys
}

🔐 Supported Key Types

Algorithm PPK v2 PPK v3 Input Decryption Output Encryption Notes
RSA ✅ (Pure JS) All key sizes
DSA ✅ (Pure JS) Standard DSA keys
ECDSA P-256 ✅ (Pure JS) secp256r1
ECDSA P-384 ✅ (Pure JS) secp384r1
ECDSA P-521 ✅ (Pure JS) secp521r1
Ed25519 ✅ (Pure JS) Pure JS encryption solution!

🔐 Pure JavaScript Encryption

This library now supports encrypting output keys with pure JavaScript for ALL key types, including Ed25519!

Encryption Support Matrix

Key Type Encryption Method Output Format Status
RSA sshpk + Node.js crypto fallback OpenSSH or PKCS#8 ✅ Fully supported
DSA sshpk + Node.js crypto fallback OpenSSH or PKCS#8 ✅ Fully supported
ECDSA sshpk + Node.js crypto fallback OpenSSH or PKCS#8 ✅ Fully supported
Ed25519 sshpk (pure JS) OpenSSH Now supported!

Key Benefits

  • No external tools required - 100% pure JavaScript solution
  • Universal Ed25519 support - Previously impossible with Node.js crypto alone
  • Secure encryption - Uses industry-standard AES-256-CBC and OpenSSH formats
  • Backward compatible - Existing code continues to work unchanged
// NEW: Encrypt any key type including Ed25519
const result = await parseFromString(ppkContent, inputPass, {
  encrypt: true,
  outputPassphrase: 'secure-password'
});

🔧 Output Formats

This library supports two output formats for private keys:

Key Type Default (PEM) Format OpenSSH Format Encrypted Format
RSA -----BEGIN RSA PRIVATE KEY----- -----BEGIN OPENSSH PRIVATE KEY----- PKCS#8 or OpenSSH
DSA -----BEGIN DSA PRIVATE KEY----- -----BEGIN OPENSSH PRIVATE KEY----- PKCS#8 or OpenSSH
ECDSA -----BEGIN EC PRIVATE KEY----- -----BEGIN OPENSSH PRIVATE KEY----- PKCS#8 or OpenSSH
Ed25519 -----BEGIN OPENSSH PRIVATE KEY----- -----BEGIN OPENSSH PRIVATE KEY----- OpenSSH only

Default Behavior (Backward Compatible):

  • parseFromFile() and parseFromString() use PEM format by default
  • No breaking changes to existing code

OpenSSH Format (ssh2-streams Compatible):

  • Use new PPKParser({ outputFormat: 'openssh' }) for modern OpenSSH format
  • Better compatibility with ssh2, ssh2-sftp-client, and similar libraries
  • Contains proper openssh-key-v1 structure

Encrypted Output:

  • Use encrypt: true option for encrypted private keys
  • Works with all key types via pure JavaScript implementation

🚀 PPK v3 Features

PPK v3 support includes all advanced security features:

  • Argon2 Key Derivation: Full support for Argon2id, Argon2i, and Argon2d variants
  • Enhanced Security: HMAC-SHA-256 MAC verification (vs SHA-1 in PPK v2)
  • AES-256-CBC Encryption: Industry-standard symmetric encryption
  • Memory-Hard Functions: Protection against brute-force attacks
  • Universal Compatibility: WebAssembly-based Argon2 works everywhere
  • Production Ready: Tested against PuTTY-generated PPK v3 files

🧪 Testing & Quality Assurance

This library is comprehensively tested with 200+ test cases covering:

Test Coverage (28 Test Keys)

  • RSA Keys: 8 variants (1024-bit v2, 2048-bit v2+v3, 4096-bit v3)
  • DSA Keys: 4 variants (1024-bit v2+v3)
  • ECDSA Keys: 11 variants (P-256 v2+v3, P-384 v2+v3, P-521 v2+v3)
  • Ed25519 Keys: 5 variants (v2+v3)

Edge Cases Tested

  • PPK Versions: Both genuine PPK v2 and v3 formats
  • Input Encryption: Unencrypted and AES-256-CBC encrypted PPK variants
  • Output Encryption: Pure JS encryption testing for ALL key types including Ed25519
  • Passphrases: Simple, complex, special characters (p@ssw0rd!#$%^&*()), 100-character long, Unicode (pásswōrd_ñeẅ_123)
  • Format Consistency: Validation between PEM and OpenSSH outputs
  • Error Handling: Wrong passphrases, corrupted files, unsupported formats
  • Encryption Validation: encrypt flag testing, output passphrase requirements, decryption verification

Test Suite Features

  • PPK Parsing: Algorithm detection, comment preservation, fingerprint generation
  • Format Conversion: Both PEM and OpenSSH output validation
  • Version Detection: Proper PPK v2 vs v3 handling
  • Security: MAC verification, passphrase handling, encryption/decryption
  • Pure JS Encryption: All 28 keys tested with encrypt flag, Ed25519 encryption validation
  • Structure Validation: Key encoding, Base64 validation, SSH format compliance

Run the test suite:

npm test                         # Complete test suite (19 tests including encryption)
npm run test:coverage            # Test suite with coverage reporting

🛠️ Advanced Usage

Custom Error Handling

const { parseFromFile, PPKError } = require('ppk-to-openssh');

async function robustConversion(filePath, passphrase) {
  try {
    return await parseFromFile(filePath, passphrase);
  } catch (error) {
    if (error instanceof PPKError) {
      switch (error.code) {
        case 'PASSPHRASE_REQUIRED':
          throw new Error('This key is encrypted. Please provide a passphrase.');
        case 'INVALID_MAC':
          throw new Error('Invalid passphrase or corrupted key file.');
        case 'FILE_NOT_FOUND':
          throw new Error(`PPK file not found: ${error.details.path}`);
        case 'WRONG_FORMAT':
          throw new Error(`This appears to be a ${error.details.hint}`);
        case 'UNSUPPORTED_ALGORITHM':
          throw new Error(`Unsupported key type: ${error.details.algorithm}`);
        default:
          throw new Error(`PPK parsing failed: ${error.message}`);
      }
    }
    throw error; // Re-throw non-PPK errors
  }
}

Environment-based Configuration

const { PPKParser } = require('ppk-to-openssh');

const parser = new PPKParser({
  maxFileSize: process.env.MAX_PPK_SIZE ? parseInt(process.env.MAX_PPK_SIZE) : 1024 * 1024,
  maxFieldSize: process.env.MAX_FIELD_SIZE ? parseInt(process.env.MAX_FIELD_SIZE) : 1024 * 1024
});

// Set via environment variables:
// MAX_PPK_SIZE=2097152 MAX_FIELD_SIZE=1048576 node myapp.js

Performance Monitoring

const { parseFromFile } = require('ppk-to-openssh');

async function timedConversion(filePath, passphrase) {
  const startTime = process.hrtime.bigint();

  try {
    const result = await parseFromFile(filePath, passphrase);
    const endTime = process.hrtime.bigint();
    const duration = Number(endTime - startTime) / 1000000; // Convert to milliseconds

    console.log(`Conversion completed in ${duration.toFixed(2)}ms`);
    return { ...result, conversionTime: duration };
  } catch (error) {
    const endTime = process.hrtime.bigint();
    const duration = Number(endTime - startTime) / 1000000;
    console.log(`Conversion failed after ${duration.toFixed(2)}ms`);
    throw error;
  }
}

🏗️ Requirements

  • Node.js: 14.0.0 or higher
  • Dependencies:
    • hash-wasm - Universal Argon2 support for PPK v3 compatibility
    • sshpk - Pure JavaScript SSH key encryption (enables Ed25519 encryption)

🌍 Environments

This library works in any JavaScript environment:

  • Node.js (14.0.0+) - Server-side applications, CLI tools, automation scripts
  • Browsers - Web applications (with bundlers like webpack, rollup, etc.)
  • VS Code Extensions - No dependency conflicts with VS Code's environment
  • Electron Apps - Desktop applications with web technologies
  • React Native - Mobile applications (with appropriate polyfills)
  • Deno - Modern JavaScript runtime (with Node.js compatibility layer)
  • Serverless Functions - AWS Lambda, Vercel, Netlify, etc.

🤝 Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

📜 License

GPL-3.0 License - see LICENSE file for details.

👨‍💻 Author

Paul C (@cartpauj)

🙏 Acknowledgments

  • PuTTY team for the PPK format specification
  • OpenSSH project for the target format standards
  • hash-wasm for universal Argon2 implementation

changelog

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[3.2.0] - 2025-06-20

Added

  • ES Modules Support: Complete conversion to ES6 imports/exports for optimal webpack bundling
  • Tree Shaking: Source code now uses import-only syntax enabling better dead code elimination
  • Build System: Enhanced build process that generates both CommonJS and ES module outputs
  • Webpack Optimization: Improved static analysis and smaller bundle sizes for modern bundlers

Enhanced

  • Developer Experience: Better import/export structure for modern development workflows
  • Bundle Performance: Optimized for webpack and other ES module-aware bundlers
  • Backward Compatibility: Maintains full CommonJS compatibility through dual exports

Technical

  • Source Migration: Converted all source files from CommonJS to ES modules
  • Build Scripts: Added automated CommonJS generation from ES module sources
  • Code Quality: Added ESLint configuration for ES module standards
  • Testing: Updated test infrastructure to work with new module system

[3.1.1] - 2025-06-19

Fixed

  • Documentation: Corrected API documentation in README.md for PPKParser class usage
  • API Reference: Fixed PPKParser.parse() method signature to show correct 2-parameter usage
  • Constructor Options: Added missing outputPassphrase option documentation for PPKParser constructor
  • API Clarity: Added clear distinction between wrapper functions and PPKParser class APIs

Enhanced

  • Examples: Updated all PPKParser code examples to show correct parameter usage
  • Developer Experience: Added warning section explaining API differences to prevent confusion

[3.1.0] - 2025-06-19

Fixed

  • Critical Encryption Bug: Fixed encryption functionality where encrypt: true option was producing unencrypted output
  • OpenSSH Key Format: Corrected cipher and KDF fields to use proper encryption values instead of 'none'
  • Key Derivation: Implemented proper bcrypt-style key derivation for OpenSSH private key encryption

Enhanced

  • Test Coverage: Added comprehensive encryption tests to verify cipher and KDF values in output
  • Error Validation: Improved error handling for encryption parameter validation
  • Encryption Tests: Added 84 new tests covering all key types with encryption functionality

Technical

  • PPKParser Options: Enhanced parser to accept and process outputPassphrase parameter
  • Encryption Algorithm: Uses AES-256-CTR cipher with bcrypt KDF for encrypted output
  • Test Suite: Expanded test suite from ~200 to 284 tests with encryption validation

[3.0.0] - 2025-06-19

Added

  • Pure JavaScript Encryption: Encrypt output keys with pure JS for ALL key types including Ed25519
  • Encrypt Flag Support: Added encrypt option to parseFromString() and parseFromFile() functions
  • Universal Ed25519 Encryption: Previously impossible Ed25519 encryption now supported via sshpk library
  • convertPPKWithEncryption(): New dedicated function for PPK conversion with encryption
  • Comprehensive Encryption Testing: All 28 test keys validated with encrypt flag functionality
  • OpenSSH Format Encryption: Ed25519 and other keys encrypted in industry-standard OpenSSH format
  • PKCS#8 Fallback: RSA/DSA/ECDSA keys support both OpenSSH and PKCS#8 encrypted formats

Enhanced

  • API Functions: parseFromString() and parseFromFile() now accept options parameter with encrypt flag
  • Error Validation: Proper validation that outputPassphrase is required when encrypt: true
  • Decryption Testing: All encrypted outputs verified to decrypt correctly with proper passphrases
  • Documentation: Complete README update with encryption examples and API reference
  • Dependencies: Added sshpk for universal SSH key encryption support

Technical

  • sshpk Integration: Pure JavaScript SSH key encryption library for Ed25519 support
  • Hybrid Approach: sshpk primary with Node.js crypto fallback for maximum compatibility
  • Test Coverage: 19 total tests including comprehensive encrypt flag validation
  • Backward Compatibility: All existing code continues to work unchanged

[2.0.0] - 2025-06-19

Added

  • Comprehensive Testing: 200+ test cases covering all PPK variants and edge cases
  • Complete DSA OpenSSH Support: Full OpenSSH format support for DSA keys
  • Enhanced Result Object: Added algorithm and comment fields to parser results
  • Extended PPK Coverage: Support for RSA 2048-bit in PPK v2, ECDSA P-384/P-521 in PPK v2
  • Edge Case Testing: Special characters, Unicode, and long passphrases support
  • Format Consistency Validation: Ensures PEM and OpenSSH outputs are equivalent

Enhanced

  • MAC Verification: Fixed passphrase handling for unencrypted keys
  • OpenSSH Format: Complete support for DSA keys in OpenSSH format
  • Test Coverage: 28 comprehensive test keys covering all possible PPK variants
  • Error Handling: Improved MAC verification logic for better reliability

Fixed

  • DSA keys now properly convert to OpenSSH format (was falling back to PEM)
  • Unencrypted keys handle unnecessary passphrases correctly
  • MAC verification uses correct passphrase for unencrypted keys (empty string)

Testing

  • PPK v2 Coverage: RSA (1024/2048), DSA (1024), ECDSA (P-256/P-384/P-521), Ed25519
  • PPK v3 Coverage: RSA (2048/4096), DSA (1024), ECDSA (P-256/P-384/P-521), Ed25519
  • Encryption Coverage: Both unencrypted and AES-256-CBC encrypted variants
  • Passphrase Testing: Standard, special characters, Unicode, 100+ character lengths
  • Format Validation: Both PEM and OpenSSH output formats thoroughly tested

Breaking Changes

  • PPKParseResult now includes algorithm and comment fields (additive, non-breaking)
  • All DSA keys now support OpenSSH format (enhancement, backward compatible)

[1.3.0] - 2024-12-19

Added

  • OpenSSH output format support for better ssh2-streams compatibility
  • Configurable output format via outputFormat option (pem/openssh)
  • Comprehensive OpenSSH private key format support for RSA and ECDSA

Fixed

  • PPK v3 unencrypted key MAC verification (critical bugfix)
  • Proper MAC key derivation for unencrypted PPK v3 files
  • Test coverage for MAC verification and output format options

Changed

  • Enhanced PPKParser constructor with outputFormat option
  • Improved backward compatibility (PEM remains default)
  • Updated documentation with OpenSSH format examples

Removed

  • Unnecessary development documentation files

[1.2.3] - 2024-12-16

Added

  • Interactive passphrase prompting in CLI for encrypted PPK files
  • Hidden input for passphrase entry (no echo to terminal)
  • Automatic detection when passphrase is required
  • Enhanced CLI help text with interactive mode documentation

Changed

  • CLI now automatically prompts for passphrases when encountering encrypted files (if no -p flag provided)
  • Updated README with interactive mode examples and usage patterns
  • Improved user experience for encrypted PPK file conversion

Technical Details

  • Pure JavaScript implementation using Node.js stdin raw mode
  • Maintains full backward compatibility with existing -p flag
  • No additional dependencies required
  • Graceful error handling for Ctrl+C and other input scenarios

[1.2.0] - 2024-12-16

Added

  • Complete PPK v3 support with Argon2id encryption
  • Universal browser and Node.js compatibility via hash-wasm integration
  • Production-ready PPK v3 file parsing for SSH connections

Changed

  • Replaced custom Argon2 implementation with proven hash-wasm library
  • Improved error handling and debugging information
  • Enhanced MAC verification for PPK v3 files

Fixed

  • PPK v3 MAC verification now works correctly with HMAC-SHA-256
  • Argon2id key derivation produces correct output for PuTTY compatibility
  • PPK v3 files with encrypted keys now decrypt properly for SSH use

Technical Details

  • Integrated hash-wasm (~11KB) for WebAssembly-based Argon2 implementation
  • Fixed MAC calculation to use algorithm + encryption + comment + keys format
  • Maintained full backward compatibility with PPK v2 files
  • Added comprehensive test coverage for PPK v3 workflows

[1.1.6] - 2024-11-06

Fixed

  • Resolved "Cannot read properties of undefined (reading '0')" crash when parsing PPK v3 files
  • Simplified Argon2 implementation to prevent memory access errors
  • PPK v3 files no longer crash the parser

Known Issues

  • PPK v3 full support still in development due to complex Argon2 requirements
  • PPK v3 files may still fail MAC verification (decryption needs correct Argon2)
  • PPK v2 files work perfectly

Notes

  • This release resolves the original crash reported by users
  • Full PPK v3 support requires a complete Argon2 implementation matching PuTTY's exact algorithm

[1.1.5] - 2024-11-06

Fixed

  • Fixed "Cannot read properties of undefined" errors in PPK v3 Argon2 processing
  • Improved Argon2 memory management and reference indexing
  • Added safety checks to prevent accessing uninitialized memory blocks
  • Resolved variable scoping issue in error handling
  • PPK v3 files now parse without crashes (MAC verification needs refinement)

Known Issues

  • PPK v3 MAC verification may fail due to simplified Argon2 implementation
  • Working on full Argon2 specification compliance in next release

[1.1.4] - 2024-11-06

Fixed

  • Fixed null reference errors in base64 regex match operations
  • Added better validation for empty or missing key data sections
  • Enhanced error reporting with detailed debugging information for troubleshooting
  • Fixed potential crashes when PPK files have malformed base64 data

[1.1.3] - 2024-11-06

Fixed

  • Fixed "Cannot read properties of undefined" errors in PPK parsing
  • Added defensive programming for all split() operations to handle malformed PPK files
  • Improved error handling for edge cases in PPK file structure parsing

[1.1.2] - 2024-11-06

Fixed

  • Fixed PPK version parsing bug that caused "Unsupported PPK version: NaN" errors
  • Improved regex pattern for parsing PPK v2 and v3 file headers

[1.1.1] - 2024-11-06

Fixed

  • Fixed README.md formatting issue with missing code block closing tag
  • Corrected markdown rendering for Requirements section

[1.1.0] - 2024-11-06

Added

  • Zero dependencies: Removed all external dependencies including argon2-browser
  • Built-in pure JavaScript Argon2 implementation for PPK v3 support
  • Universal compatibility: Now works in Node.js, browsers, VS Code extensions, and any JavaScript environment
  • Enhanced browser and VS Code extension support without dependency conflicts

Changed

  • Replaced argon2-browser dependency with built-in Argon2 implementation
  • Updated documentation to highlight zero-dependency architecture
  • Enhanced universal compatibility messaging

[1.0.0] - 2024-11-06

Added

  • Initial release of ppk-to-openssh library
  • Support for PPK versions 2 and 3
  • Support for all key types: RSA, DSA, ECDSA (P-256, P-384, P-521), Ed25519
  • Encryption support with AES-256-CBC and Argon2 key derivation
  • Comprehensive error handling with structured error codes
  • TypeScript definitions for better developer experience
  • Command-line interface for easy conversion
  • Pure JavaScript implementation with no native dependencies
  • Cross-platform compatibility (Linux, macOS, Windows)
  • Full MAC verification for security
  • Input validation and size limits
  • Both CommonJS and ES Modules support

Features

  • parseFromFile() - Convert PPK files from filesystem
  • parseFromString() - Convert PPK content from strings
  • PPKParser class with configurable options
  • PPKError class with detailed error information
  • CLI tool with comprehensive options and help
  • Support for encrypted and unencrypted keys
  • SHA256 fingerprint generation
  • Proper OpenSSH and PEM format output

Security

  • Input validation and sanitization
  • Buffer bounds checking
  • File size limits (configurable, default 1MB)
  • MAC verification for key integrity
  • Secure error handling without information leakage