Skip to content

Commit 516430a

Browse files
authored
Merge pull request #181 from ondralukes/openssh
OpenSSH key support
2 parents 5f36029 + d92a4d5 commit 516430a

File tree

8 files changed

+362
-1
lines changed

8 files changed

+362
-1
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Scheme — NodeRSA supports multiple format schemes for import/export keys:
118118

119119
* `'pkcs1'` — public key starts from `'-----BEGIN RSA PUBLIC KEY-----'` header and private key starts from `'-----BEGIN RSA PRIVATE KEY-----'` header
120120
* `'pkcs8'` — public key starts from `'-----BEGIN PUBLIC KEY-----'` header and private key starts from `'-----BEGIN PRIVATE KEY-----'` header
121+
* `'openssl'` — public key starts from `'ssh-rsa'` header and private key starts from `'-----BEGIN OPENSSH PRIVATE KEY-----'` header
121122
* `'components'` — use it for import/export key from/to raw components (see example below). For private key, importing data should contain all private key components, for public key: only public exponent (`e`) and modulus (`n`). All components (except `e`) should be Buffer, `e` could be Buffer or just normal Number.
122123

123124
Key type — can be `'private'` or `'public'`. Default `'private'`<br/>

Diff for: src/formats/formats.js

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ module.exports = {
3131
pkcs1: require('./pkcs1'),
3232
pkcs8: require('./pkcs8'),
3333
components: require('./components'),
34+
openssh: require('./openssh'),
3435

3536
isPrivateExport: function (format) {
3637
return module.exports[format] && typeof module.exports[format].privateExport === 'function';

Diff for: src/formats/openssh.js

+292
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
var _ = require('../utils')._;
2+
var utils = require('../utils');
3+
var BigInteger = require('../libs/jsbn');
4+
5+
const PRIVATE_OPENING_BOUNDARY = '-----BEGIN OPENSSH PRIVATE KEY-----';
6+
const PRIVATE_CLOSING_BOUNDARY = '-----END OPENSSH PRIVATE KEY-----';
7+
8+
module.exports = {
9+
privateExport: function (key, options) {
10+
const nbuf = key.n.toBuffer();
11+
12+
let ebuf = Buffer.alloc(4)
13+
ebuf.writeUInt32BE(key.e, 0);
14+
//Slice leading zeroes
15+
while(ebuf[0] === 0) ebuf = ebuf.slice(1);
16+
17+
const dbuf = key.d.toBuffer();
18+
const coeffbuf = key.coeff.toBuffer();
19+
const pbuf = key.p.toBuffer();
20+
const qbuf = key.q.toBuffer();
21+
let commentbuf;
22+
if(typeof key.sshcomment !== 'undefined'){
23+
commentbuf = Buffer.from(key.sshcomment);
24+
} else {
25+
commentbuf = Buffer.from([]);
26+
}
27+
28+
const pubkeyLength =
29+
11 + // 32bit length, 'ssh-rsa'
30+
4 + ebuf.byteLength +
31+
4 + nbuf.byteLength;
32+
33+
const privateKeyLength =
34+
8 + //64bit unused checksum
35+
11 + // 32bit length, 'ssh-rsa'
36+
4 + nbuf.byteLength +
37+
4 + ebuf.byteLength +
38+
4 + dbuf.byteLength +
39+
4 + coeffbuf.byteLength +
40+
4 + pbuf.byteLength +
41+
4 + qbuf.byteLength +
42+
4 + commentbuf.byteLength;
43+
44+
let length =
45+
15 + //openssh-key-v1,0x00,
46+
16 + // 2*(32bit length, 'none')
47+
4 + // 32bit length, empty string
48+
4 + // 32bit number of keys
49+
4 + // 32bit pubkey length
50+
pubkeyLength +
51+
4 + //32bit private+checksum+comment+padding length
52+
privateKeyLength;
53+
54+
const paddingLength = Math.ceil(privateKeyLength / 8)*8 - privateKeyLength;
55+
length += paddingLength;
56+
57+
const buf = Buffer.alloc(length);
58+
const writer = {buf:buf, off: 0};
59+
buf.write('openssh-key-v1', 'utf8');
60+
buf.writeUInt8(0, 14);
61+
writer.off += 15;
62+
63+
writeOpenSSHKeyString(writer, Buffer.from('none'));
64+
writeOpenSSHKeyString(writer, Buffer.from('none'));
65+
writeOpenSSHKeyString(writer, Buffer.from(''));
66+
67+
writer.off = writer.buf.writeUInt32BE(1, writer.off);
68+
writer.off = writer.buf.writeUInt32BE(pubkeyLength, writer.off);
69+
70+
writeOpenSSHKeyString(writer, Buffer.from('ssh-rsa'));
71+
writeOpenSSHKeyString(writer, ebuf);
72+
writeOpenSSHKeyString(writer, nbuf);
73+
74+
writer.off = writer.buf.writeUInt32BE(
75+
length - 47 - pubkeyLength,
76+
writer.off
77+
);
78+
writer.off += 8;
79+
80+
writeOpenSSHKeyString(writer, Buffer.from('ssh-rsa'));
81+
writeOpenSSHKeyString(writer, nbuf);
82+
writeOpenSSHKeyString(writer, ebuf);
83+
writeOpenSSHKeyString(writer, dbuf);
84+
writeOpenSSHKeyString(writer, coeffbuf);
85+
writeOpenSSHKeyString(writer, pbuf);
86+
writeOpenSSHKeyString(writer, qbuf);
87+
writeOpenSSHKeyString(writer, commentbuf);
88+
89+
let pad = 0x01;
90+
while(writer.off < length){
91+
writer.off = writer.buf.writeUInt8(pad++, writer.off);
92+
}
93+
94+
if(options.type === 'der'){
95+
return writer.buf
96+
} else {
97+
return PRIVATE_OPENING_BOUNDARY + '\n' + utils.linebrk(buf.toString('base64'), 70) + '\n' + PRIVATE_CLOSING_BOUNDARY + '\n';
98+
}
99+
},
100+
101+
privateImport: function (key, data, options) {
102+
options = options || {};
103+
var buffer;
104+
105+
if (options.type !== 'der') {
106+
if (Buffer.isBuffer(data)) {
107+
data = data.toString('utf8');
108+
}
109+
110+
if (_.isString(data)) {
111+
var pem = utils.trimSurroundingText(data, PRIVATE_OPENING_BOUNDARY, PRIVATE_CLOSING_BOUNDARY)
112+
.replace(/\s+|\n\r|\n|\r$/gm, '');
113+
buffer = Buffer.from(pem, 'base64');
114+
} else {
115+
throw Error('Unsupported key format');
116+
}
117+
} else if (Buffer.isBuffer(data)) {
118+
buffer = data;
119+
} else {
120+
throw Error('Unsupported key format');
121+
}
122+
123+
const reader = {buf:buffer, off:0};
124+
125+
if(buffer.slice(0,14).toString('ascii') !== 'openssh-key-v1')
126+
throw 'Invalid file format.';
127+
128+
reader.off += 15;
129+
130+
//ciphername
131+
if(readOpenSSHKeyString(reader).toString('ascii') !== 'none')
132+
throw Error('Unsupported key type');
133+
//kdfname
134+
if(readOpenSSHKeyString(reader).toString('ascii') !== 'none')
135+
throw Error('Unsupported key type');
136+
//kdf
137+
if(readOpenSSHKeyString(reader).toString('ascii') !== '')
138+
throw Error('Unsupported key type');
139+
//keynum
140+
reader.off += 4;
141+
142+
//sshpublength
143+
reader.off += 4;
144+
145+
//keytype
146+
if(readOpenSSHKeyString(reader).toString('ascii') !== 'ssh-rsa')
147+
throw Error('Unsupported key type');
148+
readOpenSSHKeyString(reader);
149+
readOpenSSHKeyString(reader);
150+
151+
reader.off += 12;
152+
if(readOpenSSHKeyString(reader).toString('ascii') !== 'ssh-rsa')
153+
throw Error('Unsupported key type');
154+
155+
const n = readOpenSSHKeyString(reader);
156+
const e = readOpenSSHKeyString(reader);
157+
const d = readOpenSSHKeyString(reader);
158+
const coeff = readOpenSSHKeyString(reader);
159+
const p = readOpenSSHKeyString(reader);
160+
const q = readOpenSSHKeyString(reader);
161+
162+
//Calculate missing values
163+
const dint = new BigInteger(d);
164+
const qint = new BigInteger(q);
165+
const pint = new BigInteger(p);
166+
const dp = dint.mod(pint.subtract(BigInteger.ONE));
167+
const dq = dint.mod(qint.subtract(BigInteger.ONE));
168+
169+
key.setPrivate(
170+
n, // modulus
171+
e, // publicExponent
172+
d, // privateExponent
173+
p, // prime1
174+
q, // prime2
175+
dp.toBuffer(), // exponent1 -- d mod (p1)
176+
dq.toBuffer(), // exponent2 -- d mod (q-1)
177+
coeff // coefficient -- (inverse of q) mod p
178+
);
179+
180+
key.sshcomment = readOpenSSHKeyString(reader).toString('ascii');
181+
},
182+
183+
publicExport: function (key, options) {
184+
let ebuf = Buffer.alloc(4)
185+
ebuf.writeUInt32BE(key.e, 0);
186+
//Slice leading zeroes
187+
while(ebuf[0] === 0) ebuf = ebuf.slice(1);
188+
const nbuf = key.n.toBuffer();
189+
const buf = Buffer.alloc(
190+
ebuf.byteLength + 4 +
191+
nbuf.byteLength + 4 +
192+
'ssh-rsa'.length + 4
193+
);
194+
195+
const writer = {buf: buf, off: 0};
196+
writeOpenSSHKeyString(writer, Buffer.from('ssh-rsa'));
197+
writeOpenSSHKeyString(writer, ebuf);
198+
writeOpenSSHKeyString(writer, nbuf);
199+
200+
let comment = key.sshcomment || '';
201+
202+
if(options.type === 'der'){
203+
return writer.buf
204+
} else {
205+
return 'ssh-rsa ' + buf.toString('base64') + ' ' + comment + '\n';
206+
}
207+
},
208+
209+
publicImport: function (key, data, options) {
210+
options = options || {};
211+
var buffer;
212+
213+
if (options.type !== 'der') {
214+
if (Buffer.isBuffer(data)) {
215+
data = data.toString('utf8');
216+
}
217+
218+
if (_.isString(data)) {
219+
if(data.substring(0, 8) !== 'ssh-rsa ')
220+
throw Error('Unsupported key format');
221+
let pemEnd = data.indexOf(' ', 8);
222+
223+
//Handle keys with no comment
224+
if(pemEnd === -1){
225+
pemEnd = data.length;
226+
} else {
227+
key.sshcomment = data.substring(pemEnd + 1)
228+
.replace(/\s+|\n\r|\n|\r$/gm, '');
229+
}
230+
231+
const pem = data.substring(8, pemEnd)
232+
.replace(/\s+|\n\r|\n|\r$/gm, '');
233+
buffer = Buffer.from(pem, 'base64');
234+
} else {
235+
throw Error('Unsupported key format');
236+
}
237+
} else if (Buffer.isBuffer(data)) {
238+
buffer = data;
239+
} else {
240+
throw Error('Unsupported key format');
241+
}
242+
243+
const reader = {buf:buffer, off:0};
244+
245+
const type = readOpenSSHKeyString(reader).toString('ascii');
246+
247+
if(type !== 'ssh-rsa')
248+
throw Error('Invalid key type: '+ type);
249+
250+
const e = readOpenSSHKeyString(reader);
251+
const n = readOpenSSHKeyString(reader);
252+
253+
key.setPublic(
254+
n,
255+
e
256+
);
257+
},
258+
259+
/**
260+
* Trying autodetect and import key
261+
* @param key
262+
* @param data
263+
*/
264+
autoImport: function (key, data) {
265+
// [\S\s]* matches zero or more of any character
266+
if (/^[\S\s]*-----BEGIN OPENSSH PRIVATE KEY-----\s*(?=(([A-Za-z0-9+/=]+\s*)+))\1-----END OPENSSH PRIVATE KEY-----[\S\s]*$/g.test(data)) {
267+
module.exports.privateImport(key, data);
268+
return true;
269+
}
270+
271+
if (/^[\S\s]*ssh-rsa \s*(?=(([A-Za-z0-9+/=]+\s*)+))\1[\S\s]*$/g.test(data)) {
272+
module.exports.publicImport(key, data);
273+
return true;
274+
}
275+
276+
return false;
277+
}
278+
};
279+
280+
function readOpenSSHKeyString(reader) {
281+
const len = reader.buf.readInt32BE(reader.off);
282+
reader.off += 4;
283+
const res = reader.buf.slice(reader.off, reader.off + len);
284+
reader.off += len;
285+
return res;
286+
}
287+
288+
function writeOpenSSHKeyString(writer, data) {
289+
writer.buf.writeInt32BE(data.byteLength, writer.off);
290+
writer.off += 4;
291+
writer.off += data.copy(writer.buf, writer.off);
292+
}

Diff for: test/keys/id_rsa

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
3+
NhAAAAAwEAAQAAAIEAgnWPhKQwv7z2t9+Ze+dUgTPHeS3+M+EGV+G3Pg6k2Pc1WP65jxm0
4+
5ArnipyCgKbjhjFyTmgsDR/cFH7Tbe09H6BMI5deriAvJuIcEo4zS+UyqjFXsksr9OKAbo
5+
nb++rucjYiwuCfOZW5lt1gMmJEwm5v1SWQFzSbqgpuwFVpkDEAAAH4AAAAAAAAAAAAAAAH
6+
c3NoLXJzYQAAAIEAgnWPhKQwv7z2t9+Ze+dUgTPHeS3+M+EGV+G3Pg6k2Pc1WP65jxm05A
7+
rnipyCgKbjhjFyTmgsDR/cFH7Tbe09H6BMI5deriAvJuIcEo4zS+UyqjFXsksr9OKAbonb
8+
++rucjYiwuCfOZW5lt1gMmJEwm5v1SWQFzSbqgpuwFVpkDEAAAADAQABAAAAgBGEd6D36x
9+
PT680E2Tcp+M7ghQhghKGytYdXZ6ONk9UOXLt2eLQeX4u/axfRrDRaNHLwcMjWdBPPE14t
10+
KXa5RFupnT4EBXdwhcazoCrfQBqTrSNc81Tm9VHXcsKv5lgT8hE8BCs6u5QtpwHDFK9K5R
11+
a6w5lE9nWnx3rlpxTGf9WBAAAAQANIyhoJ33ughNzbCIknkMPKtgvLOUARnbya/bkfRexL
12+
icyYzXPNuqZDY8JZQHlshN8cCcZcYjGPYYscd2LKB6oAAABBAMK1+2wf3+mtuC5DgXaU5a
13+
wvP3pqLH+OcjwWEGa6QqZ8sxeMJlhi/4OyvxMiX+KuIapxKAaQEcegZ7WeYtRngQcAAABB
14+
AKuGHAfE/QyyGFHmkviUVsCzno1Ov62HYMQSGhp9ptC3af8+5SzO4G9B+QJfuzzmo5AHZw
15+
3JQQ4csaiJxzuFbwcAAAAAAQID
16+
-----END OPENSSH PRIVATE KEY-----

Diff for: test/keys/id_rsa.pub

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCCdY+EpDC/vPa335l751SBM8d5Lf4z4QZX4bc+DqTY9zVY/rmPGbTkCueKnIKApuOGMXJOaCwNH9wUftNt7T0foEwjl16uIC8m4hwSjjNL5TKqMVeySyv04oBuidv76u5yNiLC4J85lbmW3WAyYkTCbm/VJZAXNJuqCm7AVWmQMQ==

Diff for: test/keys/id_rsa_comment

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
3+
NhAAAAAwEAAQAAAIEAs3U7i0VIEqz3K8gh67sVeM10KPoL+MmQGR9wTI0XVMb5hVzDwfTf
4+
kbMSgE2oeQQJ1A+z/m1dPANEQsKmB+4+WyexnofCTVkyaRhC4GqbWPv4J332X1MeTYs01D
5+
UMJZI/fAT9Cvq8LSDuRW02M6f+b2rAEtqHD+fxyekaBmxyjLcAAAIIAAAAAAAAAAAAAAAH
6+
c3NoLXJzYQAAAIEAs3U7i0VIEqz3K8gh67sVeM10KPoL+MmQGR9wTI0XVMb5hVzDwfTfkb
7+
MSgE2oeQQJ1A+z/m1dPANEQsKmB+4+WyexnofCTVkyaRhC4GqbWPv4J332X1MeTYs01DUM
8+
JZI/fAT9Cvq8LSDuRW02M6f+b2rAEtqHD+fxyekaBmxyjLcAAAADAQABAAAAgH63VOgub4
9+
ngYFel5W3SmILIcDFO/o0ZpopWzLEBH2xZY29r5T5bblIvI+086K0q0NXQkMQi7SanF9gc
10+
IaiP7a65Tx7lKSrAmrsnSCrZ3k+dE/+MsqGwhlDA+cxf7Ti11xSBcilcp+/KpSIEaUM8W2
11+
GWcCSRl9gY6A8rfl7bsxpBAAAAQBIPInX4FCtwwASCJVb45eMVx3+HWnMIzCW6cCn24scY
12+
mXw4AaO/ykDpcMtyDRv8T6id7fkR+XKqZ6lKP+HxaC4AAABBANhJFHqKlpbN0PTfqjyyBM
13+
ZWzKyzHEjwPvUcrPIrSsuQNGz/+Ync0zte0nQXMBYSQxIiSJ32fvwVdcE/hv9rWa8AAABB
14+
ANRpALkbvpU4pjNYfWX/74eu9cbUDhHbu74cJq0mmU3jd4Uv4X7wUijkG4lVfsrdpXzJRv
15+
aMkt1MrDSzj7kMp3kAAAAQb25kcmFAb25kcmFsdWtlcwECAw==
16+
-----END OPENSSH PRIVATE KEY-----

Diff for: test/keys/id_rsa_comment.pub

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCzdTuLRUgSrPcryCHruxV4zXQo+gv4yZAZH3BMjRdUxvmFXMPB9N+RsxKATah5BAnUD7P+bV08A0RCwqYH7j5bJ7Geh8JNWTJpGELgaptY+/gnffZfUx5NizTUNQwlkj98BP0K+rwtIO5FbTYzp/5vasAS2ocP5/HJ6RoGbHKMtw== ondra@ondralukes

Diff for: test/tests.js

+34-1
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,10 @@ describe('NodeRSA', function () {
294294
'pkcs8-private': {public: false, der: false, file: 'private_pkcs8.pem'},
295295
'pkcs8-der': {public: false, der: true, file: 'private_pkcs8.der'},
296296
'pkcs1-public': {public: true, der: false, file: 'public_pkcs1.pem'},
297-
'pkcs8-public': {public: true, der: false, file: 'public_pkcs8.pem'}
297+
'pkcs8-public': {public: true, der: false, file: 'public_pkcs8.pem'},
298+
299+
'openssh-public': {public: true, der: false, file: 'id_rsa.pub'},
300+
'openssh-private': {public: false, der: false, file: 'id_rsa'}
298301
};
299302

300303
describe('Good cases', function () {
@@ -483,6 +486,36 @@ describe('NodeRSA', function () {
483486
})(format);
484487
}
485488
});
489+
490+
describe('OpenSSH keys', function () {
491+
/*
492+
* Warning!
493+
* OpenSSH private key contains unused 64bit value, this value is set by ssh-keygen,
494+
* but it's not used. NodeRSA does NOT store this value, so importing and exporting key sets this value to 0.
495+
* This value is 0 in test files, so the tests pass.
496+
*/
497+
it('key export should preserve key data including comment', function(){
498+
const opensshPrivateKey = fs.readFileSync(keysFolder + 'id_rsa_comment').toString();
499+
const opensshPublicKey = fs.readFileSync(keysFolder + 'id_rsa_comment.pub').toString();
500+
const opensshPriv = new NodeRSA(opensshPrivateKey);
501+
const opensshPub = new NodeRSA(opensshPublicKey);
502+
503+
assert.equal(
504+
opensshPriv.exportKey('openssh-private'),
505+
opensshPrivateKey
506+
);
507+
508+
assert.equal(
509+
opensshPriv.exportKey('openssh-public'),
510+
opensshPublicKey
511+
);
512+
513+
assert.equal(
514+
opensshPub.exportKey('openssh-public'),
515+
opensshPublicKey
516+
);
517+
});
518+
})
486519
});
487520

488521
describe('Bad cases', function () {

0 commit comments

Comments
 (0)