Skip to content

Commit

Permalink
#27 Add support for PuTTY PPK format
Browse files Browse the repository at this point in the history
Reviewed by: Isaac Davis <isaac.davis@joyent.com>
  • Loading branch information
arekinath committed Dec 20, 2018
1 parent 44aec4a commit f647cf2
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 16 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -126,6 +126,9 @@ Parameters
- `rfc4253`: raw OpenSSH wire format
- `openssh`: new post-OpenSSH 6.5 internal format, produced by
`ssh-keygen -o`
- `dnssec`: `.key` file format output by `dnssec-keygen` etc
- `putty`: the PuTTY `.ppk` file format (supports truncated variant without
all the lines from `Private-Lines:` onwards)
- `options` -- Optional Object, extra options, with keys:
- `filename` -- Optional String, name for the key being parsed
(eg. the filename that was opened). Used to generate
Expand Down Expand Up @@ -234,6 +237,7 @@ Parameters
`ssh-keygen -o`
- `pkcs1`, `pkcs8`: variants of `pem`
- `rfc4253`: raw OpenSSH wire format
- `dnssec`: `.private` format output by `dnssec-keygen` etc.
- `options` -- Optional Object, extra options, with keys:
- `filename` -- Optional String, name for the key being parsed
(eg. the filename that was opened). Used to generate
Expand Down
35 changes: 20 additions & 15 deletions bin/sshpk-conv
Expand Up @@ -97,6 +97,7 @@ if (require.main === module) {
console.error(' - openssh like output of ssh-keygen -o');
console.error(' - rfc4253 raw OpenSSH wire format');
console.error(' - dnssec dnssec-keygen format');
console.error(' - putty PuTTY ppk format');
console.error('\navailable fingerprint formats:');
console.error(' - hex colon-separated hex for SSH');
console.error(' straight hex for SPKI');
Expand Down Expand Up @@ -130,9 +131,7 @@ if (require.main === module) {
inFile = fs.createReadStream(inFilePath);
}
} catch (e) {
console.error('sshpk-conv: error opening input file' +
': ' + e.name + ': ' + e.message);
process.exit(1);
ifError(e, 'error opening input file');
}

var outFile = process.stdout;
Expand All @@ -143,9 +142,7 @@ if (require.main === module) {
outFile = fs.createWriteStream(opts.out);
}
} catch (e) {
console.error('sshpk-conv: error opening output file' +
': ' + e.name + ': ' + e.message);
process.exit(1);
ifError(e, 'error opening output file');
}

var bufs = [];
Expand All @@ -169,20 +166,14 @@ if (require.main === module) {
} catch (e) {
if (e.name === 'KeyEncryptedError') {
getPassword(function (err, pw) {
if (err) {
console.log('sshpk-conv: ' +
err.name + ': ' +
err.message);
process.exit(1);
}
if (err)
ifError(err);
parseOpts.passphrase = pw;
processKey();
});
return;
}
console.error('sshpk-conv: ' +
e.name + ': ' + e.message);
process.exit(1);
ifError(e);
}

if (opts.derive)
Expand Down Expand Up @@ -236,3 +227,17 @@ if (require.main === module) {
});
});
}

function ifError(e, txt) {
if (txt)
txt = txt + ': ';
else
txt = '';
console.error('sshpk-conv: ' + txt + e.name + ': ' + e.message);
if (process.env['DEBUG'] || process.env['V']) {
console.error(e.stack);
if (e.innerErr)
console.error(e.innerErr.stack);
}
process.exit(1);
}
19 changes: 18 additions & 1 deletion lib/formats/auto.js
@@ -1,4 +1,4 @@
// Copyright 2015 Joyent, Inc.
// Copyright 2018 Joyent, Inc.

module.exports = {
read: read,
Expand All @@ -15,6 +15,7 @@ var pem = require('./pem');
var ssh = require('./ssh');
var rfc4253 = require('./rfc4253');
var dnssec = require('./dnssec');
var putty = require('./putty');

var DNSSEC_PRIVKEY_HEADER_PREFIX = 'Private-key-format: v1';

Expand All @@ -26,6 +27,8 @@ function read(buf, options) {
return (ssh.read(buf, options));
if (buf.match(/^\s*ecdsa-/))
return (ssh.read(buf, options));
if (buf.match(/^putty-user-key-file-2:/i))
return (putty.read(buf, options));
if (findDNSSECHeader(buf))
return (dnssec.read(buf, options));
buf = Buffer.from(buf, 'binary');
Expand All @@ -35,6 +38,8 @@ function read(buf, options) {
return (pem.read(buf, options));
if (findSSHHeader(buf))
return (ssh.read(buf, options));
if (findPuTTYHeader(buf))
return (putty.read(buf, options));
if (findDNSSECHeader(buf))
return (dnssec.read(buf, options));
}
Expand All @@ -43,6 +48,18 @@ function read(buf, options) {
throw (new Error('Failed to auto-detect format of key'));
}

function findPuTTYHeader(buf) {
var offset = 0;
while (offset < buf.length &&
(buf[offset] === 32 || buf[offset] === 10 || buf[offset] === 9))
++offset;
if (offset + 22 <= buf.length &&
buf.slice(offset, offset + 22).toString('ascii').toLowerCase() ===
'putty-user-key-file-2:')
return (true);
return (false);
}

function findSSHHeader(buf) {
var offset = 0;
while (offset < buf.length &&
Expand Down
99 changes: 99 additions & 0 deletions lib/formats/putty.js
@@ -0,0 +1,99 @@
// Copyright 2018 Joyent, Inc.

module.exports = {
read: read,
write: write
};

var assert = require('assert-plus');
var Buffer = require('safer-buffer').Buffer;
var rfc4253 = require('./rfc4253');
var Key = require('../key');

var errors = require('../errors');

function read(buf, options) {
var lines = buf.toString('ascii').split(/[\r\n]+/);
var found = false;
var parts;
var si = 0;
while (si < lines.length) {
parts = splitHeader(lines[si++]);
if (parts &&
parts[0].toLowerCase() === 'putty-user-key-file-2') {
found = true;
break;
}
}
if (!found) {
throw (new Error('No PuTTY format first line found'));
}
var alg = parts[1];

parts = splitHeader(lines[si++]);
assert.equal(parts[0].toLowerCase(), 'encryption');

parts = splitHeader(lines[si++]);
assert.equal(parts[0].toLowerCase(), 'comment');
var comment = parts[1];

parts = splitHeader(lines[si++]);
assert.equal(parts[0].toLowerCase(), 'public-lines');
var publicLines = parseInt(parts[1], 10);
if (!isFinite(publicLines) || publicLines < 0 ||
publicLines > lines.length) {
throw (new Error('Invalid public-lines count'));
}

var publicBuf = Buffer.from(
lines.slice(si, si + publicLines).join(''), 'base64');
var keyType = rfc4253.algToKeyType(alg);
var key = rfc4253.read(publicBuf);
if (key.type !== keyType) {
throw (new Error('Outer key algorithm mismatch'));
}
key.comment = comment;
return (key);
}

function splitHeader(line) {
var idx = line.indexOf(':');
if (idx === -1)
return (null);
var header = line.slice(0, idx);
++idx;
while (line[idx] === ' ')
++idx;
var rest = line.slice(idx);
return ([header, rest]);
}

function write(key, options) {
assert.object(key);
if (!Key.isKey(key))
throw (new Error('Must be a public key'));

var alg = rfc4253.keyTypeToAlg(key);
var buf = rfc4253.write(key);
var comment = key.comment || '';

var b64 = buf.toString('base64');
var lines = wrap(b64, 64);

lines.unshift('Public-Lines: ' + lines.length);
lines.unshift('Comment: ' + comment);
lines.unshift('Encryption: none');
lines.unshift('PuTTY-User-Key-File-2: ' + alg);

return (Buffer.from(lines.join('\n') + '\n'));
}

function wrap(txt, len) {
var lines = [];
var pos = 0;
while (pos < txt.length) {
lines.push(txt.slice(pos, pos + 64));
pos += 64;
}
return (lines);
}
2 changes: 2 additions & 0 deletions lib/key.js
Expand Up @@ -32,6 +32,8 @@ formats['ssh'] = require('./formats/ssh');
formats['ssh-private'] = require('./formats/ssh-private');
formats['openssh'] = formats['ssh-private'];
formats['dnssec'] = require('./formats/dnssec');
formats['putty'] = require('./formats/putty');
formats['ppk'] = formats['putty'];

function Key(opts) {
assert.object(opts, 'options');
Expand Down
1 change: 1 addition & 0 deletions test/assets/dsa-ppk.pub
@@ -0,0 +1 @@
ssh-dss AAAAB3NzaC1kc3MAAAEBALPLp37KKWyxxCtV085ozT7tY1zQimFhPvD+rPO4SSfHquoRM/szwOXAqyavqYggajNyzSN/x5+gLu4p2iTmS0jC0Nf2FB5tyqxBKsrubmhVMZpK+dBLm2V7NTE6fJpGRmpTNCKWPOfdZP7wF0w8oRNaTz1ILjNlXh06zXrp4FU+wv0ums1PemF5Ff7u77vzkxXDsYFOO+btgAFEO8oTBapG1H0ZkcvTzIStCY6SnbzDggPtaqasKLjh4zocvdR9M4E1IIFtMaFSZx19w6ccpzAFD7dqTqNVavSN2SzSzcjMeEdaLiSceof8qu5IcLdrofuQ0ltCVKNFrMm9pZK2kvUAAAAVANmWKeGafd0C0vAgGthSALs1q/q1AAABAQCtHTj9zydZ2VDhHxrge+WeSF/Ix0zUknwDo7luJGgSTb/BZg42W+RIsWo+qFeK6XEzuaqlXIOL7ekBFkDvwOGt1l13T6YF4xH1XvjKg4+YP/U+QMqgiv/+eBodmm14vZqDUD37C1JXMr/vJ+6xDYlvJwwUdmBUcU0PMyw70YHEtWeGuzXbvMDsKLztXQrYJqzjKX7pyf0x2Rj3g7/HSdIssP32Da2lflNs0VPqCWjsG1bDiU8oqdOHhekfM+khqvVNIxmI/6nW/Izax/8ORv0QSmPxFp0FmTaRtNFInLDnOnshXHf5h9+eC4vLmDs2P/65FLfS5WfQMABPTdifsLG6AAABAQCeT7XwWZ5+OMoGYgh2WeTAmHCykXnhnuH0LvyRoeuJq+oX3QJyU/I5L71mjQ1eCqMa/7RYZ3J2ct34ZQ7qiJzgbUgPvKuFuRgf12gn7cCc97Mu/Fo/Mjm0iisYigAR/QbT5uIpIASr5z8JxdNaEgEzhwP3o3+YviXXwEAbN36ZBwLp54MODbz+n5xGqG9avE5i4VCOkP5Q3ng6nnn+kelyaIHsWd498YJEQcs0eKaYlux03/D0hD/q6HLVtob6qjUN7xo0xFcYKQBzkBz0CgQ/CvgfKFOrS8Z/EdS6d6dFB8yKvEXygRPdE9MSepPlfy1d9Vvys8k2fs+Ny72wgg5I dsa-key-20170331
22 changes: 22 additions & 0 deletions test/assets/dsa-pub-err.ppk
@@ -0,0 +1,22 @@
PuTTY-User-Key-File-2: ssh-dss
Encryption: none
Comment: dsa-key-20170331
Public-Lines: 200
AAAAB3NzaC1kc3MAAAEBALPLp37KKWyxxCtV085ozT7tY1zQimFhPvD+rPO4SSfH
quoRM/szwOXAqyavqYggajNyzSN/x5+gLu4p2iTmS0jC0Nf2FB5tyqxBKsrubmhV
MZpK+dBLm2V7NTE6fJpGRmpTNCKWPOfdZP7wF0w8oRNaTz1ILjNlXh06zXrp4FU+
wv0ums1PemF5Ff7u77vzkxXDsYFOO+btgAFEO8oTBapG1H0ZkcvTzIStCY6SnbzD
ggPtaqasKLjh4zocvdR9M4E1IIFtMaFSZx19w6ccpzAFD7dqTqNVavSN2SzSzcjM
eEdaLiSceof8qu5IcLdrofuQ0ltCVKNFrMm9pZK2kvUAAAAVANmWKeGafd0C0vAg
GthSALs1q/q1AAABAQCtHTj9zydZ2VDhHxrge+WeSF/Ix0zUknwDo7luJGgSTb/B
Zg42W+RIsWo+qFeK6XEzuaqlXIOL7ekBFkDvwOGt1l13T6YF4xH1XvjKg4+YP/U+
QMqgiv/+eBodmm14vZqDUD37C1JXMr/vJ+6xDYlvJwwUdmBUcU0PMyw70YHEtWeG
uzXbvMDsKLztXQrYJqzjKX7pyf0x2Rj3g7/HSdIssP32Da2lflNs0VPqCWjsG1bD
iU8oqdOHhekfM+khqvVNIxmI/6nW/Izax/8ORv0QSmPxFp0FmTaRtNFInLDnOnsh
XHf5h9+eC4vLmDs2P/65FLfS5WfQMABPTdifsLG6AAABAQCeT7XwWZ5+OMoGYgh2
WeTAmHCykXnhnuH0LvyRoeuJq+oX3QJyU/I5L71mjQ1eCqMa/7RYZ3J2ct34ZQ7q
iJzgbUgPvKuFuRgf12gn7cCc97Mu/Fo/Mjm0iisYigAR/QbT5uIpIASr5z8JxdNa
EgEzhwP3o3+YviXXwEAbN36ZBwLp54MODbz+n5xGqG9avE5i4VCOkP5Q3ng6nnn+
kelyaIHsWd498YJEQcs0eKaYlux03/D0hD/q6HLVtob6qjUN7xo0xFcYKQBzkBz0
CgQ/CvgfKFOrS8Z/EdS6d6dFB8yKvEXygRPdE9MSepPlfy1d9Vvys8k2fs+Ny72w
gg5I
22 changes: 22 additions & 0 deletions test/assets/dsa-pub.ppk
@@ -0,0 +1,22 @@
PuTTY-User-Key-File-2: ssh-dss
Encryption: none
Comment: dsa-key-20170331
Public-Lines: 18
AAAAB3NzaC1kc3MAAAEBALPLp37KKWyxxCtV085ozT7tY1zQimFhPvD+rPO4SSfH
quoRM/szwOXAqyavqYggajNyzSN/x5+gLu4p2iTmS0jC0Nf2FB5tyqxBKsrubmhV
MZpK+dBLm2V7NTE6fJpGRmpTNCKWPOfdZP7wF0w8oRNaTz1ILjNlXh06zXrp4FU+
wv0ums1PemF5Ff7u77vzkxXDsYFOO+btgAFEO8oTBapG1H0ZkcvTzIStCY6SnbzD
ggPtaqasKLjh4zocvdR9M4E1IIFtMaFSZx19w6ccpzAFD7dqTqNVavSN2SzSzcjM
eEdaLiSceof8qu5IcLdrofuQ0ltCVKNFrMm9pZK2kvUAAAAVANmWKeGafd0C0vAg
GthSALs1q/q1AAABAQCtHTj9zydZ2VDhHxrge+WeSF/Ix0zUknwDo7luJGgSTb/B
Zg42W+RIsWo+qFeK6XEzuaqlXIOL7ekBFkDvwOGt1l13T6YF4xH1XvjKg4+YP/U+
QMqgiv/+eBodmm14vZqDUD37C1JXMr/vJ+6xDYlvJwwUdmBUcU0PMyw70YHEtWeG
uzXbvMDsKLztXQrYJqzjKX7pyf0x2Rj3g7/HSdIssP32Da2lflNs0VPqCWjsG1bD
iU8oqdOHhekfM+khqvVNIxmI/6nW/Izax/8ORv0QSmPxFp0FmTaRtNFInLDnOnsh
XHf5h9+eC4vLmDs2P/65FLfS5WfQMABPTdifsLG6AAABAQCeT7XwWZ5+OMoGYgh2
WeTAmHCykXnhnuH0LvyRoeuJq+oX3QJyU/I5L71mjQ1eCqMa/7RYZ3J2ct34ZQ7q
iJzgbUgPvKuFuRgf12gn7cCc97Mu/Fo/Mjm0iisYigAR/QbT5uIpIASr5z8JxdNa
EgEzhwP3o3+YviXXwEAbN36ZBwLp54MODbz+n5xGqG9avE5i4VCOkP5Q3ng6nnn+
kelyaIHsWd498YJEQcs0eKaYlux03/D0hD/q6HLVtob6qjUN7xo0xFcYKQBzkBz0
CgQ/CvgfKFOrS8Z/EdS6d6dFB8yKvEXygRPdE9MSepPlfy1d9Vvys8k2fs+Ny72w
gg5I
25 changes: 25 additions & 0 deletions test/assets/dsa.ppk
@@ -0,0 +1,25 @@
PuTTY-User-Key-File-2: ssh-dss
Encryption: aes256-cbc
Comment: dsa-key-20170331
Public-Lines: 18
AAAAB3NzaC1kc3MAAAEBALPLp37KKWyxxCtV085ozT7tY1zQimFhPvD+rPO4SSfH
quoRM/szwOXAqyavqYggajNyzSN/x5+gLu4p2iTmS0jC0Nf2FB5tyqxBKsrubmhV
MZpK+dBLm2V7NTE6fJpGRmpTNCKWPOfdZP7wF0w8oRNaTz1ILjNlXh06zXrp4FU+
wv0ums1PemF5Ff7u77vzkxXDsYFOO+btgAFEO8oTBapG1H0ZkcvTzIStCY6SnbzD
ggPtaqasKLjh4zocvdR9M4E1IIFtMaFSZx19w6ccpzAFD7dqTqNVavSN2SzSzcjM
eEdaLiSceof8qu5IcLdrofuQ0ltCVKNFrMm9pZK2kvUAAAAVANmWKeGafd0C0vAg
GthSALs1q/q1AAABAQCtHTj9zydZ2VDhHxrge+WeSF/Ix0zUknwDo7luJGgSTb/B
Zg42W+RIsWo+qFeK6XEzuaqlXIOL7ekBFkDvwOGt1l13T6YF4xH1XvjKg4+YP/U+
QMqgiv/+eBodmm14vZqDUD37C1JXMr/vJ+6xDYlvJwwUdmBUcU0PMyw70YHEtWeG
uzXbvMDsKLztXQrYJqzjKX7pyf0x2Rj3g7/HSdIssP32Da2lflNs0VPqCWjsG1bD
iU8oqdOHhekfM+khqvVNIxmI/6nW/Izax/8ORv0QSmPxFp0FmTaRtNFInLDnOnsh
XHf5h9+eC4vLmDs2P/65FLfS5WfQMABPTdifsLG6AAABAQCeT7XwWZ5+OMoGYgh2
WeTAmHCykXnhnuH0LvyRoeuJq+oX3QJyU/I5L71mjQ1eCqMa/7RYZ3J2ct34ZQ7q
iJzgbUgPvKuFuRgf12gn7cCc97Mu/Fo/Mjm0iisYigAR/QbT5uIpIASr5z8JxdNa
EgEzhwP3o3+YviXXwEAbN36ZBwLp54MODbz+n5xGqG9avE5i4VCOkP5Q3ng6nnn+
kelyaIHsWd498YJEQcs0eKaYlux03/D0hD/q6HLVtob6qjUN7xo0xFcYKQBzkBz0
CgQ/CvgfKFOrS8Z/EdS6d6dFB8yKvEXygRPdE9MSepPlfy1d9Vvys8k2fs+Ny72w
gg5I
Private-Lines: 1
KSl/bN/cgkBjVRhd91zBbTlSPNqRwQJ5xDF4qrehUtQ=
Private-MAC: 3ab460bd517d8f4747f837a37710f88ad95be6ea
26 changes: 26 additions & 0 deletions test/assets/rsa.ppk
@@ -0,0 +1,26 @@
PuTTY-User-Key-File-2: ssh-rsa
Encryption: aes256-cbc
Comment: rsa-key-20170331
Public-Lines: 6
AAAAB3NzaC1yc2EAAAABJQAAAQEAsJb8crG6hSSizpKs8EQip90n+n4MKs6qYKIt
r4X8EeBhKMbQNXGbLC617Ui0cVQMF8nfEGe61/Fc0uQOWJdEk2ANe6rtcaFPTwYH
LyPznd28c9xfBifqksdBjqH+Svr57MBqmivQi3gTIXeIlGYyXhKh9U8J2WDIpMks
hE1UmsqvJgMjoqNVWG/iU+t4GeKxdDd4TGIHiZU6JfGf777pAB2+Uhi7PrSvs7Ov
oetgNW7LFsPiA8zbwfj5jkVQ2ycHoAOHMuthB3bkNetL+A6V6pkfDLTVd9g0SiPF
K03ut1XIw+FtxO5ioEfy5XvwkIPbAl2vInfxL0EacWFkXq08ew==
Private-Lines: 14
lfdred2syPlQV1MrIG+3uAKVIrd8Wlprhxyco0A3LEbEjaUJOMaRY4GG+47MUdfG
YKqtH1zWuvpBrlfmLiUySL2j4DustFvMkLD9aFI4IFUPPCbV0ujuQh1rFQuutzDj
dmOFdQAcwV0/clB2ewz/tX5x0GPQq71xrLHq4qQLepaDbZFIhF+S6K+opXiSJPc6
YG6SJKMBj+jshFrHeONN+MLTCPxDb/Ulrh3Fl5no1xLjjZEtO/TETeKyeM3jUAu2
oNn8WI9MP2wEGgp2Nux8759BveUr4zH71lS9OKagG1ipxMkyikrgAZpXbxTVFXsq
m+5RkZuvd9smYVJl3S2T1c/YcS5SaSaC4FTYEOw70N68Mw2nOQEWpXkdnxqUMdbp
o209XxOQdYsbBi3+FWfcY/jr846XstkRx2wj5PfGFn/ULIoaMNIpYHgP2JgSsz+G
rX5ZXktpCzHRBfhudqyOAzvlWqyQS5+hBpigqupUbrRHJKmuGNYkhay9W/Ka80pX
+K7yom1k6JD8lK+GA+AvU+v2pxKMsvoxVDBgJUfvg4Su8wVyPj8g01DSvrM9Ld4X
1nXbA+kEijokalDsxhcp6ZjYWBFwMn+kBZ7ztBlwKf5gnoMa9A917mJiUNy1B+t8
CwYuBNilNUWSrWCcoG29c9HPyz8QVkFPfMpqhX3W43G2a3pRi2CZzwEHV+te4TDz
pmpU2yObRH93nWVnFn91onKL0rHPEI4613e7Nhb8R1dhXtJZoLPbjdLJlmWUgZa/
VDfhatovJ7XzD4WRaamJaevK6LUnmULDZRkzU9fJJBLp76BJvbENiQ6GANGp4i2h
bJ0sbiWMFEnKTFGpcIVLZI11xLJ7H9S6TrDJFVhVSGB114Dj0TjIdA8FyOOik77o
Private-MAC: e528058a4a0851024975d88a0b012bb8b40607b0

0 comments on commit f647cf2

Please sign in to comment.