Skip to content

Commit f647cf2

Browse files
author
Alex Wilson
committed
#27 Add support for PuTTY PPK format
Reviewed by: Isaac Davis <[email protected]>
1 parent 44aec4a commit f647cf2

File tree

11 files changed

+315
-16
lines changed

11 files changed

+315
-16
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ Parameters
126126
- `rfc4253`: raw OpenSSH wire format
127127
- `openssh`: new post-OpenSSH 6.5 internal format, produced by
128128
`ssh-keygen -o`
129+
- `dnssec`: `.key` file format output by `dnssec-keygen` etc
130+
- `putty`: the PuTTY `.ppk` file format (supports truncated variant without
131+
all the lines from `Private-Lines:` onwards)
129132
- `options` -- Optional Object, extra options, with keys:
130133
- `filename` -- Optional String, name for the key being parsed
131134
(eg. the filename that was opened). Used to generate
@@ -234,6 +237,7 @@ Parameters
234237
`ssh-keygen -o`
235238
- `pkcs1`, `pkcs8`: variants of `pem`
236239
- `rfc4253`: raw OpenSSH wire format
240+
- `dnssec`: `.private` format output by `dnssec-keygen` etc.
237241
- `options` -- Optional Object, extra options, with keys:
238242
- `filename` -- Optional String, name for the key being parsed
239243
(eg. the filename that was opened). Used to generate

bin/sshpk-conv

+20-15
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ if (require.main === module) {
9797
console.error(' - openssh like output of ssh-keygen -o');
9898
console.error(' - rfc4253 raw OpenSSH wire format');
9999
console.error(' - dnssec dnssec-keygen format');
100+
console.error(' - putty PuTTY ppk format');
100101
console.error('\navailable fingerprint formats:');
101102
console.error(' - hex colon-separated hex for SSH');
102103
console.error(' straight hex for SPKI');
@@ -130,9 +131,7 @@ if (require.main === module) {
130131
inFile = fs.createReadStream(inFilePath);
131132
}
132133
} catch (e) {
133-
console.error('sshpk-conv: error opening input file' +
134-
': ' + e.name + ': ' + e.message);
135-
process.exit(1);
134+
ifError(e, 'error opening input file');
136135
}
137136

138137
var outFile = process.stdout;
@@ -143,9 +142,7 @@ if (require.main === module) {
143142
outFile = fs.createWriteStream(opts.out);
144143
}
145144
} catch (e) {
146-
console.error('sshpk-conv: error opening output file' +
147-
': ' + e.name + ': ' + e.message);
148-
process.exit(1);
145+
ifError(e, 'error opening output file');
149146
}
150147

151148
var bufs = [];
@@ -169,20 +166,14 @@ if (require.main === module) {
169166
} catch (e) {
170167
if (e.name === 'KeyEncryptedError') {
171168
getPassword(function (err, pw) {
172-
if (err) {
173-
console.log('sshpk-conv: ' +
174-
err.name + ': ' +
175-
err.message);
176-
process.exit(1);
177-
}
169+
if (err)
170+
ifError(err);
178171
parseOpts.passphrase = pw;
179172
processKey();
180173
});
181174
return;
182175
}
183-
console.error('sshpk-conv: ' +
184-
e.name + ': ' + e.message);
185-
process.exit(1);
176+
ifError(e);
186177
}
187178

188179
if (opts.derive)
@@ -236,3 +227,17 @@ if (require.main === module) {
236227
});
237228
});
238229
}
230+
231+
function ifError(e, txt) {
232+
if (txt)
233+
txt = txt + ': ';
234+
else
235+
txt = '';
236+
console.error('sshpk-conv: ' + txt + e.name + ': ' + e.message);
237+
if (process.env['DEBUG'] || process.env['V']) {
238+
console.error(e.stack);
239+
if (e.innerErr)
240+
console.error(e.innerErr.stack);
241+
}
242+
process.exit(1);
243+
}

lib/formats/auto.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015 Joyent, Inc.
1+
// Copyright 2018 Joyent, Inc.
22

33
module.exports = {
44
read: read,
@@ -15,6 +15,7 @@ var pem = require('./pem');
1515
var ssh = require('./ssh');
1616
var rfc4253 = require('./rfc4253');
1717
var dnssec = require('./dnssec');
18+
var putty = require('./putty');
1819

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

@@ -26,6 +27,8 @@ function read(buf, options) {
2627
return (ssh.read(buf, options));
2728
if (buf.match(/^\s*ecdsa-/))
2829
return (ssh.read(buf, options));
30+
if (buf.match(/^putty-user-key-file-2:/i))
31+
return (putty.read(buf, options));
2932
if (findDNSSECHeader(buf))
3033
return (dnssec.read(buf, options));
3134
buf = Buffer.from(buf, 'binary');
@@ -35,6 +38,8 @@ function read(buf, options) {
3538
return (pem.read(buf, options));
3639
if (findSSHHeader(buf))
3740
return (ssh.read(buf, options));
41+
if (findPuTTYHeader(buf))
42+
return (putty.read(buf, options));
3843
if (findDNSSECHeader(buf))
3944
return (dnssec.read(buf, options));
4045
}
@@ -43,6 +48,18 @@ function read(buf, options) {
4348
throw (new Error('Failed to auto-detect format of key'));
4449
}
4550

51+
function findPuTTYHeader(buf) {
52+
var offset = 0;
53+
while (offset < buf.length &&
54+
(buf[offset] === 32 || buf[offset] === 10 || buf[offset] === 9))
55+
++offset;
56+
if (offset + 22 <= buf.length &&
57+
buf.slice(offset, offset + 22).toString('ascii').toLowerCase() ===
58+
'putty-user-key-file-2:')
59+
return (true);
60+
return (false);
61+
}
62+
4663
function findSSHHeader(buf) {
4764
var offset = 0;
4865
while (offset < buf.length &&

lib/formats/putty.js

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2018 Joyent, Inc.
2+
3+
module.exports = {
4+
read: read,
5+
write: write
6+
};
7+
8+
var assert = require('assert-plus');
9+
var Buffer = require('safer-buffer').Buffer;
10+
var rfc4253 = require('./rfc4253');
11+
var Key = require('../key');
12+
13+
var errors = require('../errors');
14+
15+
function read(buf, options) {
16+
var lines = buf.toString('ascii').split(/[\r\n]+/);
17+
var found = false;
18+
var parts;
19+
var si = 0;
20+
while (si < lines.length) {
21+
parts = splitHeader(lines[si++]);
22+
if (parts &&
23+
parts[0].toLowerCase() === 'putty-user-key-file-2') {
24+
found = true;
25+
break;
26+
}
27+
}
28+
if (!found) {
29+
throw (new Error('No PuTTY format first line found'));
30+
}
31+
var alg = parts[1];
32+
33+
parts = splitHeader(lines[si++]);
34+
assert.equal(parts[0].toLowerCase(), 'encryption');
35+
36+
parts = splitHeader(lines[si++]);
37+
assert.equal(parts[0].toLowerCase(), 'comment');
38+
var comment = parts[1];
39+
40+
parts = splitHeader(lines[si++]);
41+
assert.equal(parts[0].toLowerCase(), 'public-lines');
42+
var publicLines = parseInt(parts[1], 10);
43+
if (!isFinite(publicLines) || publicLines < 0 ||
44+
publicLines > lines.length) {
45+
throw (new Error('Invalid public-lines count'));
46+
}
47+
48+
var publicBuf = Buffer.from(
49+
lines.slice(si, si + publicLines).join(''), 'base64');
50+
var keyType = rfc4253.algToKeyType(alg);
51+
var key = rfc4253.read(publicBuf);
52+
if (key.type !== keyType) {
53+
throw (new Error('Outer key algorithm mismatch'));
54+
}
55+
key.comment = comment;
56+
return (key);
57+
}
58+
59+
function splitHeader(line) {
60+
var idx = line.indexOf(':');
61+
if (idx === -1)
62+
return (null);
63+
var header = line.slice(0, idx);
64+
++idx;
65+
while (line[idx] === ' ')
66+
++idx;
67+
var rest = line.slice(idx);
68+
return ([header, rest]);
69+
}
70+
71+
function write(key, options) {
72+
assert.object(key);
73+
if (!Key.isKey(key))
74+
throw (new Error('Must be a public key'));
75+
76+
var alg = rfc4253.keyTypeToAlg(key);
77+
var buf = rfc4253.write(key);
78+
var comment = key.comment || '';
79+
80+
var b64 = buf.toString('base64');
81+
var lines = wrap(b64, 64);
82+
83+
lines.unshift('Public-Lines: ' + lines.length);
84+
lines.unshift('Comment: ' + comment);
85+
lines.unshift('Encryption: none');
86+
lines.unshift('PuTTY-User-Key-File-2: ' + alg);
87+
88+
return (Buffer.from(lines.join('\n') + '\n'));
89+
}
90+
91+
function wrap(txt, len) {
92+
var lines = [];
93+
var pos = 0;
94+
while (pos < txt.length) {
95+
lines.push(txt.slice(pos, pos + 64));
96+
pos += 64;
97+
}
98+
return (lines);
99+
}

lib/key.js

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ formats['ssh'] = require('./formats/ssh');
3232
formats['ssh-private'] = require('./formats/ssh-private');
3333
formats['openssh'] = formats['ssh-private'];
3434
formats['dnssec'] = require('./formats/dnssec');
35+
formats['putty'] = require('./formats/putty');
36+
formats['ppk'] = formats['putty'];
3537

3638
function Key(opts) {
3739
assert.object(opts, 'options');

test/assets/dsa-ppk.pub

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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

test/assets/dsa-pub-err.ppk

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
PuTTY-User-Key-File-2: ssh-dss
2+
Encryption: none
3+
Comment: dsa-key-20170331
4+
Public-Lines: 200
5+
AAAAB3NzaC1kc3MAAAEBALPLp37KKWyxxCtV085ozT7tY1zQimFhPvD+rPO4SSfH
6+
quoRM/szwOXAqyavqYggajNyzSN/x5+gLu4p2iTmS0jC0Nf2FB5tyqxBKsrubmhV
7+
MZpK+dBLm2V7NTE6fJpGRmpTNCKWPOfdZP7wF0w8oRNaTz1ILjNlXh06zXrp4FU+
8+
wv0ums1PemF5Ff7u77vzkxXDsYFOO+btgAFEO8oTBapG1H0ZkcvTzIStCY6SnbzD
9+
ggPtaqasKLjh4zocvdR9M4E1IIFtMaFSZx19w6ccpzAFD7dqTqNVavSN2SzSzcjM
10+
eEdaLiSceof8qu5IcLdrofuQ0ltCVKNFrMm9pZK2kvUAAAAVANmWKeGafd0C0vAg
11+
GthSALs1q/q1AAABAQCtHTj9zydZ2VDhHxrge+WeSF/Ix0zUknwDo7luJGgSTb/B
12+
Zg42W+RIsWo+qFeK6XEzuaqlXIOL7ekBFkDvwOGt1l13T6YF4xH1XvjKg4+YP/U+
13+
QMqgiv/+eBodmm14vZqDUD37C1JXMr/vJ+6xDYlvJwwUdmBUcU0PMyw70YHEtWeG
14+
uzXbvMDsKLztXQrYJqzjKX7pyf0x2Rj3g7/HSdIssP32Da2lflNs0VPqCWjsG1bD
15+
iU8oqdOHhekfM+khqvVNIxmI/6nW/Izax/8ORv0QSmPxFp0FmTaRtNFInLDnOnsh
16+
XHf5h9+eC4vLmDs2P/65FLfS5WfQMABPTdifsLG6AAABAQCeT7XwWZ5+OMoGYgh2
17+
WeTAmHCykXnhnuH0LvyRoeuJq+oX3QJyU/I5L71mjQ1eCqMa/7RYZ3J2ct34ZQ7q
18+
iJzgbUgPvKuFuRgf12gn7cCc97Mu/Fo/Mjm0iisYigAR/QbT5uIpIASr5z8JxdNa
19+
EgEzhwP3o3+YviXXwEAbN36ZBwLp54MODbz+n5xGqG9avE5i4VCOkP5Q3ng6nnn+
20+
kelyaIHsWd498YJEQcs0eKaYlux03/D0hD/q6HLVtob6qjUN7xo0xFcYKQBzkBz0
21+
CgQ/CvgfKFOrS8Z/EdS6d6dFB8yKvEXygRPdE9MSepPlfy1d9Vvys8k2fs+Ny72w
22+
gg5I

test/assets/dsa-pub.ppk

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
PuTTY-User-Key-File-2: ssh-dss
2+
Encryption: none
3+
Comment: dsa-key-20170331
4+
Public-Lines: 18
5+
AAAAB3NzaC1kc3MAAAEBALPLp37KKWyxxCtV085ozT7tY1zQimFhPvD+rPO4SSfH
6+
quoRM/szwOXAqyavqYggajNyzSN/x5+gLu4p2iTmS0jC0Nf2FB5tyqxBKsrubmhV
7+
MZpK+dBLm2V7NTE6fJpGRmpTNCKWPOfdZP7wF0w8oRNaTz1ILjNlXh06zXrp4FU+
8+
wv0ums1PemF5Ff7u77vzkxXDsYFOO+btgAFEO8oTBapG1H0ZkcvTzIStCY6SnbzD
9+
ggPtaqasKLjh4zocvdR9M4E1IIFtMaFSZx19w6ccpzAFD7dqTqNVavSN2SzSzcjM
10+
eEdaLiSceof8qu5IcLdrofuQ0ltCVKNFrMm9pZK2kvUAAAAVANmWKeGafd0C0vAg
11+
GthSALs1q/q1AAABAQCtHTj9zydZ2VDhHxrge+WeSF/Ix0zUknwDo7luJGgSTb/B
12+
Zg42W+RIsWo+qFeK6XEzuaqlXIOL7ekBFkDvwOGt1l13T6YF4xH1XvjKg4+YP/U+
13+
QMqgiv/+eBodmm14vZqDUD37C1JXMr/vJ+6xDYlvJwwUdmBUcU0PMyw70YHEtWeG
14+
uzXbvMDsKLztXQrYJqzjKX7pyf0x2Rj3g7/HSdIssP32Da2lflNs0VPqCWjsG1bD
15+
iU8oqdOHhekfM+khqvVNIxmI/6nW/Izax/8ORv0QSmPxFp0FmTaRtNFInLDnOnsh
16+
XHf5h9+eC4vLmDs2P/65FLfS5WfQMABPTdifsLG6AAABAQCeT7XwWZ5+OMoGYgh2
17+
WeTAmHCykXnhnuH0LvyRoeuJq+oX3QJyU/I5L71mjQ1eCqMa/7RYZ3J2ct34ZQ7q
18+
iJzgbUgPvKuFuRgf12gn7cCc97Mu/Fo/Mjm0iisYigAR/QbT5uIpIASr5z8JxdNa
19+
EgEzhwP3o3+YviXXwEAbN36ZBwLp54MODbz+n5xGqG9avE5i4VCOkP5Q3ng6nnn+
20+
kelyaIHsWd498YJEQcs0eKaYlux03/D0hD/q6HLVtob6qjUN7xo0xFcYKQBzkBz0
21+
CgQ/CvgfKFOrS8Z/EdS6d6dFB8yKvEXygRPdE9MSepPlfy1d9Vvys8k2fs+Ny72w
22+
gg5I

test/assets/dsa.ppk

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
PuTTY-User-Key-File-2: ssh-dss
2+
Encryption: aes256-cbc
3+
Comment: dsa-key-20170331
4+
Public-Lines: 18
5+
AAAAB3NzaC1kc3MAAAEBALPLp37KKWyxxCtV085ozT7tY1zQimFhPvD+rPO4SSfH
6+
quoRM/szwOXAqyavqYggajNyzSN/x5+gLu4p2iTmS0jC0Nf2FB5tyqxBKsrubmhV
7+
MZpK+dBLm2V7NTE6fJpGRmpTNCKWPOfdZP7wF0w8oRNaTz1ILjNlXh06zXrp4FU+
8+
wv0ums1PemF5Ff7u77vzkxXDsYFOO+btgAFEO8oTBapG1H0ZkcvTzIStCY6SnbzD
9+
ggPtaqasKLjh4zocvdR9M4E1IIFtMaFSZx19w6ccpzAFD7dqTqNVavSN2SzSzcjM
10+
eEdaLiSceof8qu5IcLdrofuQ0ltCVKNFrMm9pZK2kvUAAAAVANmWKeGafd0C0vAg
11+
GthSALs1q/q1AAABAQCtHTj9zydZ2VDhHxrge+WeSF/Ix0zUknwDo7luJGgSTb/B
12+
Zg42W+RIsWo+qFeK6XEzuaqlXIOL7ekBFkDvwOGt1l13T6YF4xH1XvjKg4+YP/U+
13+
QMqgiv/+eBodmm14vZqDUD37C1JXMr/vJ+6xDYlvJwwUdmBUcU0PMyw70YHEtWeG
14+
uzXbvMDsKLztXQrYJqzjKX7pyf0x2Rj3g7/HSdIssP32Da2lflNs0VPqCWjsG1bD
15+
iU8oqdOHhekfM+khqvVNIxmI/6nW/Izax/8ORv0QSmPxFp0FmTaRtNFInLDnOnsh
16+
XHf5h9+eC4vLmDs2P/65FLfS5WfQMABPTdifsLG6AAABAQCeT7XwWZ5+OMoGYgh2
17+
WeTAmHCykXnhnuH0LvyRoeuJq+oX3QJyU/I5L71mjQ1eCqMa/7RYZ3J2ct34ZQ7q
18+
iJzgbUgPvKuFuRgf12gn7cCc97Mu/Fo/Mjm0iisYigAR/QbT5uIpIASr5z8JxdNa
19+
EgEzhwP3o3+YviXXwEAbN36ZBwLp54MODbz+n5xGqG9avE5i4VCOkP5Q3ng6nnn+
20+
kelyaIHsWd498YJEQcs0eKaYlux03/D0hD/q6HLVtob6qjUN7xo0xFcYKQBzkBz0
21+
CgQ/CvgfKFOrS8Z/EdS6d6dFB8yKvEXygRPdE9MSepPlfy1d9Vvys8k2fs+Ny72w
22+
gg5I
23+
Private-Lines: 1
24+
KSl/bN/cgkBjVRhd91zBbTlSPNqRwQJ5xDF4qrehUtQ=
25+
Private-MAC: 3ab460bd517d8f4747f837a37710f88ad95be6ea

test/assets/rsa.ppk

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
PuTTY-User-Key-File-2: ssh-rsa
2+
Encryption: aes256-cbc
3+
Comment: rsa-key-20170331
4+
Public-Lines: 6
5+
AAAAB3NzaC1yc2EAAAABJQAAAQEAsJb8crG6hSSizpKs8EQip90n+n4MKs6qYKIt
6+
r4X8EeBhKMbQNXGbLC617Ui0cVQMF8nfEGe61/Fc0uQOWJdEk2ANe6rtcaFPTwYH
7+
LyPznd28c9xfBifqksdBjqH+Svr57MBqmivQi3gTIXeIlGYyXhKh9U8J2WDIpMks
8+
hE1UmsqvJgMjoqNVWG/iU+t4GeKxdDd4TGIHiZU6JfGf777pAB2+Uhi7PrSvs7Ov
9+
oetgNW7LFsPiA8zbwfj5jkVQ2ycHoAOHMuthB3bkNetL+A6V6pkfDLTVd9g0SiPF
10+
K03ut1XIw+FtxO5ioEfy5XvwkIPbAl2vInfxL0EacWFkXq08ew==
11+
Private-Lines: 14
12+
lfdred2syPlQV1MrIG+3uAKVIrd8Wlprhxyco0A3LEbEjaUJOMaRY4GG+47MUdfG
13+
YKqtH1zWuvpBrlfmLiUySL2j4DustFvMkLD9aFI4IFUPPCbV0ujuQh1rFQuutzDj
14+
dmOFdQAcwV0/clB2ewz/tX5x0GPQq71xrLHq4qQLepaDbZFIhF+S6K+opXiSJPc6
15+
YG6SJKMBj+jshFrHeONN+MLTCPxDb/Ulrh3Fl5no1xLjjZEtO/TETeKyeM3jUAu2
16+
oNn8WI9MP2wEGgp2Nux8759BveUr4zH71lS9OKagG1ipxMkyikrgAZpXbxTVFXsq
17+
m+5RkZuvd9smYVJl3S2T1c/YcS5SaSaC4FTYEOw70N68Mw2nOQEWpXkdnxqUMdbp
18+
o209XxOQdYsbBi3+FWfcY/jr846XstkRx2wj5PfGFn/ULIoaMNIpYHgP2JgSsz+G
19+
rX5ZXktpCzHRBfhudqyOAzvlWqyQS5+hBpigqupUbrRHJKmuGNYkhay9W/Ka80pX
20+
+K7yom1k6JD8lK+GA+AvU+v2pxKMsvoxVDBgJUfvg4Su8wVyPj8g01DSvrM9Ld4X
21+
1nXbA+kEijokalDsxhcp6ZjYWBFwMn+kBZ7ztBlwKf5gnoMa9A917mJiUNy1B+t8
22+
CwYuBNilNUWSrWCcoG29c9HPyz8QVkFPfMpqhX3W43G2a3pRi2CZzwEHV+te4TDz
23+
pmpU2yObRH93nWVnFn91onKL0rHPEI4613e7Nhb8R1dhXtJZoLPbjdLJlmWUgZa/
24+
VDfhatovJ7XzD4WRaamJaevK6LUnmULDZRkzU9fJJBLp76BJvbENiQ6GANGp4i2h
25+
bJ0sbiWMFEnKTFGpcIVLZI11xLJ7H9S6TrDJFVhVSGB114Dj0TjIdA8FyOOik77o
26+
Private-MAC: e528058a4a0851024975d88a0b012bb8b40607b0

0 commit comments

Comments
 (0)