putty.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. // Copyright 2018 Joyent, Inc.
  2. module.exports = {
  3. read: read,
  4. write: write
  5. };
  6. var assert = require('assert-plus');
  7. var Buffer = require('safer-buffer').Buffer;
  8. var rfc4253 = require('./rfc4253');
  9. var Key = require('../key');
  10. var SSHBuffer = require('../ssh-buffer');
  11. var crypto = require('crypto');
  12. var PrivateKey = require('../private-key');
  13. var errors = require('../errors');
  14. // https://tartarus.org/~simon/putty-prerel-snapshots/htmldoc/AppendixC.html
  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. var formatVersion;
  21. while (si < lines.length) {
  22. parts = splitHeader(lines[si++]);
  23. if (parts) {
  24. formatVersion = {
  25. 'putty-user-key-file-2': 2,
  26. 'putty-user-key-file-3': 3
  27. }[parts[0].toLowerCase()];
  28. if (formatVersion) {
  29. found = true;
  30. break;
  31. }
  32. }
  33. }
  34. if (!found) {
  35. throw (new Error('No PuTTY format first line found'));
  36. }
  37. var alg = parts[1];
  38. parts = splitHeader(lines[si++]);
  39. assert.equal(parts[0].toLowerCase(), 'encryption');
  40. var encryption = parts[1];
  41. parts = splitHeader(lines[si++]);
  42. assert.equal(parts[0].toLowerCase(), 'comment');
  43. var comment = parts[1];
  44. parts = splitHeader(lines[si++]);
  45. assert.equal(parts[0].toLowerCase(), 'public-lines');
  46. var publicLines = parseInt(parts[1], 10);
  47. if (!isFinite(publicLines) || publicLines < 0 ||
  48. publicLines > lines.length) {
  49. throw (new Error('Invalid public-lines count'));
  50. }
  51. var publicBuf = Buffer.from(
  52. lines.slice(si, si + publicLines).join(''), 'base64');
  53. var keyType = rfc4253.algToKeyType(alg);
  54. var key = rfc4253.read(publicBuf);
  55. if (key.type !== keyType) {
  56. throw (new Error('Outer key algorithm mismatch'));
  57. }
  58. si += publicLines;
  59. if (lines[si]) {
  60. parts = splitHeader(lines[si++]);
  61. assert.equal(parts[0].toLowerCase(), 'private-lines');
  62. var privateLines = parseInt(parts[1], 10);
  63. if (!isFinite(privateLines) || privateLines < 0 ||
  64. privateLines > lines.length) {
  65. throw (new Error('Invalid private-lines count'));
  66. }
  67. var privateBuf = Buffer.from(
  68. lines.slice(si, si + privateLines).join(''), 'base64');
  69. if (encryption !== 'none' && formatVersion === 3) {
  70. throw new Error('Encrypted keys arenot supported for' +
  71. ' PuTTY format version 3');
  72. }
  73. if (encryption === 'aes256-cbc') {
  74. if (!options.passphrase) {
  75. throw (new errors.KeyEncryptedError(
  76. options.filename, 'PEM'));
  77. }
  78. var iv = Buffer.alloc(16, 0);
  79. var decipher = crypto.createDecipheriv(
  80. 'aes-256-cbc',
  81. derivePPK2EncryptionKey(options.passphrase),
  82. iv);
  83. decipher.setAutoPadding(false);
  84. privateBuf = Buffer.concat([
  85. decipher.update(privateBuf), decipher.final()]);
  86. }
  87. key = new PrivateKey(key);
  88. if (key.type !== keyType) {
  89. throw (new Error('Outer key algorithm mismatch'));
  90. }
  91. var sshbuf = new SSHBuffer({buffer: privateBuf});
  92. var privateKeyParts;
  93. if (alg === 'ssh-dss') {
  94. privateKeyParts = [ {
  95. name: 'x',
  96. data: sshbuf.readBuffer()
  97. }];
  98. } else if (alg === 'ssh-rsa') {
  99. privateKeyParts = [
  100. { name: 'd', data: sshbuf.readBuffer() },
  101. { name: 'p', data: sshbuf.readBuffer() },
  102. { name: 'q', data: sshbuf.readBuffer() },
  103. { name: 'iqmp', data: sshbuf.readBuffer() }
  104. ];
  105. } else if (alg.match(/^ecdsa-sha2-nistp/)) {
  106. privateKeyParts = [ {
  107. name: 'd', data: sshbuf.readBuffer()
  108. } ];
  109. } else if (alg === 'ssh-ed25519') {
  110. privateKeyParts = [ {
  111. name: 'k', data: sshbuf.readBuffer()
  112. } ];
  113. } else {
  114. throw new Error('Unsupported PPK key type: ' + alg);
  115. }
  116. key = new PrivateKey({
  117. type: key.type,
  118. parts: key.parts.concat(privateKeyParts)
  119. });
  120. }
  121. key.comment = comment;
  122. return (key);
  123. }
  124. function derivePPK2EncryptionKey(passphrase) {
  125. var hash1 = crypto.createHash('sha1').update(Buffer.concat([
  126. Buffer.from([0, 0, 0, 0]),
  127. Buffer.from(passphrase)
  128. ])).digest();
  129. var hash2 = crypto.createHash('sha1').update(Buffer.concat([
  130. Buffer.from([0, 0, 0, 1]),
  131. Buffer.from(passphrase)
  132. ])).digest();
  133. return (Buffer.concat([hash1, hash2]).slice(0, 32));
  134. }
  135. function splitHeader(line) {
  136. var idx = line.indexOf(':');
  137. if (idx === -1)
  138. return (null);
  139. var header = line.slice(0, idx);
  140. ++idx;
  141. while (line[idx] === ' ')
  142. ++idx;
  143. var rest = line.slice(idx);
  144. return ([header, rest]);
  145. }
  146. function write(key, options) {
  147. assert.object(key);
  148. if (!Key.isKey(key))
  149. throw (new Error('Must be a public key'));
  150. var alg = rfc4253.keyTypeToAlg(key);
  151. var buf = rfc4253.write(key);
  152. var comment = key.comment || '';
  153. var b64 = buf.toString('base64');
  154. var lines = wrap(b64, 64);
  155. lines.unshift('Public-Lines: ' + lines.length);
  156. lines.unshift('Comment: ' + comment);
  157. lines.unshift('Encryption: none');
  158. lines.unshift('PuTTY-User-Key-File-2: ' + alg);
  159. return (Buffer.from(lines.join('\n') + '\n'));
  160. }
  161. function wrap(txt, len) {
  162. var lines = [];
  163. var pos = 0;
  164. while (pos < txt.length) {
  165. lines.push(txt.slice(pos, pos + 64));
  166. pos += 64;
  167. }
  168. return (lines);
  169. }