You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
5.4 KiB

  1. // Copyright 2018 Joyent, Inc.
  2. module.exports = Fingerprint;
  3. var assert = require('assert-plus');
  4. var Buffer = require('safer-buffer').Buffer;
  5. var algs = require('./algs');
  6. var crypto = require('crypto');
  7. var errs = require('./errors');
  8. var Key = require('./key');
  9. var PrivateKey = require('./private-key');
  10. var Certificate = require('./certificate');
  11. var utils = require('./utils');
  12. var FingerprintFormatError = errs.FingerprintFormatError;
  13. var InvalidAlgorithmError = errs.InvalidAlgorithmError;
  14. function Fingerprint(opts) {
  15. assert.object(opts, 'options');
  16. assert.string(opts.type, 'options.type');
  17. assert.buffer(opts.hash, 'options.hash');
  18. assert.string(opts.algorithm, 'options.algorithm');
  19. this.algorithm = opts.algorithm.toLowerCase();
  20. if (algs.hashAlgs[this.algorithm] !== true)
  21. throw (new InvalidAlgorithmError(this.algorithm));
  22. this.hash = opts.hash;
  23. this.type = opts.type;
  24. this.hashType = opts.hashType;
  25. }
  26. Fingerprint.prototype.toString = function (format) {
  27. if (format === undefined) {
  28. if (this.algorithm === 'md5' || this.hashType === 'spki')
  29. format = 'hex';
  30. else
  31. format = 'base64';
  32. }
  33. assert.string(format);
  34. switch (format) {
  35. case 'hex':
  36. if (this.hashType === 'spki')
  37. return (this.hash.toString('hex'));
  38. return (addColons(this.hash.toString('hex')));
  39. case 'base64':
  40. if (this.hashType === 'spki')
  41. return (this.hash.toString('base64'));
  42. return (sshBase64Format(this.algorithm,
  43. this.hash.toString('base64')));
  44. default:
  45. throw (new FingerprintFormatError(undefined, format));
  46. }
  47. };
  48. Fingerprint.prototype.matches = function (other) {
  49. assert.object(other, 'key or certificate');
  50. if (this.type === 'key' && this.hashType !== 'ssh') {
  51. utils.assertCompatible(other, Key, [1, 7], 'key with spki');
  52. if (PrivateKey.isPrivateKey(other)) {
  53. utils.assertCompatible(other, PrivateKey, [1, 6],
  54. 'privatekey with spki support');
  55. }
  56. } else if (this.type === 'key') {
  57. utils.assertCompatible(other, Key, [1, 0], 'key');
  58. } else {
  59. utils.assertCompatible(other, Certificate, [1, 0],
  60. 'certificate');
  61. }
  62. var theirHash = other.hash(this.algorithm, this.hashType);
  63. var theirHash2 = crypto.createHash(this.algorithm).
  64. update(theirHash).digest('base64');
  65. if (this.hash2 === undefined)
  66. this.hash2 = crypto.createHash(this.algorithm).
  67. update(this.hash).digest('base64');
  68. return (this.hash2 === theirHash2);
  69. };
  70. /*JSSTYLED*/
  71. var base64RE = /^[A-Za-z0-9+\/=]+$/;
  72. /*JSSTYLED*/
  73. var hexRE = /^[a-fA-F0-9]+$/;
  74. Fingerprint.parse = function (fp, options) {
  75. assert.string(fp, 'fingerprint');
  76. var alg, hash, enAlgs;
  77. if (Array.isArray(options)) {
  78. enAlgs = options;
  79. options = {};
  80. }
  81. assert.optionalObject(options, 'options');
  82. if (options === undefined)
  83. options = {};
  84. if (options.enAlgs !== undefined)
  85. enAlgs = options.enAlgs;
  86. if (options.algorithms !== undefined)
  87. enAlgs = options.algorithms;
  88. assert.optionalArrayOfString(enAlgs, 'algorithms');
  89. var hashType = 'ssh';
  90. if (options.hashType !== undefined)
  91. hashType = options.hashType;
  92. assert.string(hashType, 'options.hashType');
  93. var parts = fp.split(':');
  94. if (parts.length == 2) {
  95. alg = parts[0].toLowerCase();
  96. if (!base64RE.test(parts[1]))
  97. throw (new FingerprintFormatError(fp));
  98. try {
  99. hash = Buffer.from(parts[1], 'base64');
  100. } catch (e) {
  101. throw (new FingerprintFormatError(fp));
  102. }
  103. } else if (parts.length > 2) {
  104. alg = 'md5';
  105. if (parts[0].toLowerCase() === 'md5')
  106. parts = parts.slice(1);
  107. parts = parts.map(function (p) {
  108. while (p.length < 2)
  109. p = '0' + p;
  110. if (p.length > 2)
  111. throw (new FingerprintFormatError(fp));
  112. return (p);
  113. });
  114. parts = parts.join('');
  115. if (!hexRE.test(parts) || parts.length % 2 !== 0)
  116. throw (new FingerprintFormatError(fp));
  117. try {
  118. hash = Buffer.from(parts, 'hex');
  119. } catch (e) {
  120. throw (new FingerprintFormatError(fp));
  121. }
  122. } else {
  123. if (hexRE.test(fp)) {
  124. hash = Buffer.from(fp, 'hex');
  125. } else if (base64RE.test(fp)) {
  126. hash = Buffer.from(fp, 'base64');
  127. } else {
  128. throw (new FingerprintFormatError(fp));
  129. }
  130. switch (hash.length) {
  131. case 32:
  132. alg = 'sha256';
  133. break;
  134. case 16:
  135. alg = 'md5';
  136. break;
  137. case 20:
  138. alg = 'sha1';
  139. break;
  140. case 64:
  141. alg = 'sha512';
  142. break;
  143. default:
  144. throw (new FingerprintFormatError(fp));
  145. }
  146. /* Plain hex/base64: guess it's probably SPKI unless told. */
  147. if (options.hashType === undefined)
  148. hashType = 'spki';
  149. }
  150. if (alg === undefined)
  151. throw (new FingerprintFormatError(fp));
  152. if (algs.hashAlgs[alg] === undefined)
  153. throw (new InvalidAlgorithmError(alg));
  154. if (enAlgs !== undefined) {
  155. enAlgs = enAlgs.map(function (a) { return a.toLowerCase(); });
  156. if (enAlgs.indexOf(alg) === -1)
  157. throw (new InvalidAlgorithmError(alg));
  158. }
  159. return (new Fingerprint({
  160. algorithm: alg,
  161. hash: hash,
  162. type: options.type || 'key',
  163. hashType: hashType
  164. }));
  165. };
  166. function addColons(s) {
  167. /*JSSTYLED*/
  168. return (s.replace(/(.{2})(?=.)/g, '$1:'));
  169. }
  170. function base64Strip(s) {
  171. /*JSSTYLED*/
  172. return (s.replace(/=*$/, ''));
  173. }
  174. function sshBase64Format(alg, h) {
  175. return (alg.toUpperCase() + ':' + base64Strip(h));
  176. }
  177. Fingerprint.isFingerprint = function (obj, ver) {
  178. return (utils.isCompatible(obj, Fingerprint, ver));
  179. };
  180. /*
  181. * API versions for Fingerprint:
  182. * [1,0] -- initial ver
  183. * [1,1] -- first tagged ver
  184. * [1,2] -- hashType and spki support
  185. */
  186. Fingerprint.prototype._sshpkApiVersion = [1, 2];
  187. Fingerprint._oldVersionDetect = function (obj) {
  188. assert.func(obj.toString);
  189. assert.func(obj.matches);
  190. return ([1, 0]);
  191. };