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.
- 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
- 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
npm install ppk-to-openssh
The library uses hash-wasm for universal Argon2 support, ensuring PPK v3 compatibility across all JavaScript environments.
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
npm run build
- Build both CommonJS and ES modulesnpm run build:cjs
- Build CommonJS version onlynpm run build:esm
- Build ES modules version onlynpm run build:types
- Copy TypeScript definitionsnpm test
- Run the test suitenpm run clean
- Remove built files
# 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
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
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);
}
}
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);
}
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');
}
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);
}
}
}
const { PPKParser } = require('ppk-to-openssh');
async function advancedUsage() {
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');
console.log('Algorithm:', result.algorithm);
if (result.curve) {
console.log('Curve:', result.curve);
}
// Access parser info
console.log('Supported algorithms:', parser.supportedAlgorithms);
}
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');
// 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
});
}
// 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)
}
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');
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}`);
}
}
// 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);
}
}
});
// 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}`);
}
}
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'
});
}
}
});
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
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);
}
}
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)
- encrypt
- Returns
Promise<PPKParseResult>
- Conversion result - Throws
PPKError
- On parsing errors
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)
- encrypt
- Returns
Promise<PPKParseResult>
- Conversion result - Throws
PPKError
- On parsing errors
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
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
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'
)
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 descriptioncode
(string): Structured error code for programmatic handlingdetails
(object): Additional error context and hints
Common Error Codes:
INVALID_INPUT
- Invalid input parametersFILE_NOT_FOUND
- PPK file not foundWRONG_FORMAT
- Not a PPK file (OpenSSH/PEM detected)INVALID_PPK_FORMAT
- Missing PPK headerUNSUPPORTED_VERSION
- Unsupported PPK versionPASSPHRASE_REQUIRED
- Encrypted key needs passphraseINVALID_MAC
- Wrong passphrase or corrupted fileUNSUPPORTED_ALGORITHM
- Unsupported key algorithmFILE_TOO_LARGE
- File exceeds size limitUNSUPPORTED_ARGON2
- Unsupported Argon2 variantUNSUPPORTED_ENCRYPTION
- Unsupported encryption method
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
}
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! |
This library now supports encrypting output keys with pure JavaScript for ALL key types, including Ed25519!
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! |
- 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'
});
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()
andparseFromString()
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 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
This library is comprehensively tested with 200+ test cases covering:
- 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)
- 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
- 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
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
}
}
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
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;
}
}
- Node.js: 14.0.0 or higher
- Dependencies:
hash-wasm
- Universal Argon2 support for PPK v3 compatibilitysshpk
- Pure JavaScript SSH key encryption (enables Ed25519 encryption)
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.
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
GPL-3.0 License - see LICENSE file for details.
Paul C (@cartpauj)
- PuTTY team for the PPK format specification
- OpenSSH project for the target format standards
- hash-wasm for universal Argon2 implementation