// Copyright 2018 Joyent, Inc.

module.exports = Fingerprint;

var assert = require('assert-plus'); var Buffer = require('safer-buffer').Buffer; var algs = require('./algs'); var crypto = require('crypto'); var errs = require('./errors'); var Key = require('./key'); var PrivateKey = require('./private-key'); var Certificate = require('./certificate'); var utils = require('./utils');

var FingerprintFormatError = errs.FingerprintFormatError; var InvalidAlgorithmError = errs.InvalidAlgorithmError;

function Fingerprint(opts) {

assert.object(opts, 'options');
assert.string(opts.type, 'options.type');
assert.buffer(opts.hash, 'options.hash');
assert.string(opts.algorithm, 'options.algorithm');

this.algorithm = opts.algorithm.toLowerCase();
if (algs.hashAlgs[this.algorithm] !== true)
        throw (new InvalidAlgorithmError(this.algorithm));

this.hash = opts.hash;
this.type = opts.type;
this.hashType = opts.hashType;

}

Fingerprint.prototype.toString = function (format) {

if (format === undefined) {
        if (this.algorithm === 'md5' || this.hashType === 'spki')
                format = 'hex';
        else
                format = 'base64';
}
assert.string(format);

switch (format) {
case 'hex':
        if (this.hashType === 'spki')
                return (this.hash.toString('hex'));
        return (addColons(this.hash.toString('hex')));
case 'base64':
        if (this.hashType === 'spki')
                return (this.hash.toString('base64'));
        return (sshBase64Format(this.algorithm,
            this.hash.toString('base64')));
default:
        throw (new FingerprintFormatError(undefined, format));
}

};

Fingerprint.prototype.matches = function (other) {

assert.object(other, 'key or certificate');
if (this.type === 'key' && this.hashType !== 'ssh') {
        utils.assertCompatible(other, Key, [1, 7], 'key with spki');
        if (PrivateKey.isPrivateKey(other)) {
                utils.assertCompatible(other, PrivateKey, [1, 6],
                    'privatekey with spki support');
        }
} else if (this.type === 'key') {
        utils.assertCompatible(other, Key, [1, 0], 'key');
} else {
        utils.assertCompatible(other, Certificate, [1, 0],
            'certificate');
}

var theirHash = other.hash(this.algorithm, this.hashType);
var theirHash2 = crypto.createHash(this.algorithm).
    update(theirHash).digest('base64');

if (this.hash2 === undefined)
        this.hash2 = crypto.createHash(this.algorithm).
            update(this.hash).digest('base64');

return (this.hash2 === theirHash2);

};

/JSSTYLED/ var base64RE = /^[A-Za-z0-9+/=]+$/; /JSSTYLED/ var hexRE = /^[a-fA-F0-9]+$/;

Fingerprint.parse = function (fp, options) {

assert.string(fp, 'fingerprint');

var alg, hash, enAlgs;
if (Array.isArray(options)) {
        enAlgs = options;
        options = {};
}
assert.optionalObject(options, 'options');
if (options === undefined)
        options = {};
if (options.enAlgs !== undefined)
        enAlgs = options.enAlgs;
if (options.algorithms !== undefined)
        enAlgs = options.algorithms;
assert.optionalArrayOfString(enAlgs, 'algorithms');

var hashType = 'ssh';
if (options.hashType !== undefined)
        hashType = options.hashType;
assert.string(hashType, 'options.hashType');

var parts = fp.split(':');
if (parts.length == 2) {
        alg = parts[0].toLowerCase();
        if (!base64RE.test(parts[1]))
                throw (new FingerprintFormatError(fp));
        try {
                hash = Buffer.from(parts[1], 'base64');
        } catch (e) {
                throw (new FingerprintFormatError(fp));
        }
} else if (parts.length > 2) {
        alg = 'md5';
        if (parts[0].toLowerCase() === 'md5')
                parts = parts.slice(1);
        parts = parts.map(function (p) {
                while (p.length < 2)
                        p = '0' + p;
                if (p.length > 2)
                        throw (new FingerprintFormatError(fp));
                return (p);
        });
        parts = parts.join('');
        if (!hexRE.test(parts) || parts.length % 2 !== 0)
                throw (new FingerprintFormatError(fp));
        try {
                hash = Buffer.from(parts, 'hex');
        } catch (e) {
                throw (new FingerprintFormatError(fp));
        }
} else {
        if (hexRE.test(fp)) {
                hash = Buffer.from(fp, 'hex');
        } else if (base64RE.test(fp)) {
                hash = Buffer.from(fp, 'base64');
        } else {
                throw (new FingerprintFormatError(fp));
        }

        switch (hash.length) {
        case 32:
                alg = 'sha256';
                break;
        case 16:
                alg = 'md5';
                break;
        case 20:
                alg = 'sha1';
                break;
        case 64:
                alg = 'sha512';
                break;
        default:
                throw (new FingerprintFormatError(fp));
        }

        /* Plain hex/base64: guess it's probably SPKI unless told. */
        if (options.hashType === undefined)
                hashType = 'spki';
}

if (alg === undefined)
        throw (new FingerprintFormatError(fp));

if (algs.hashAlgs[alg] === undefined)
        throw (new InvalidAlgorithmError(alg));

if (enAlgs !== undefined) {
        enAlgs = enAlgs.map(function (a) { return a.toLowerCase(); });
        if (enAlgs.indexOf(alg) === -1)
                throw (new InvalidAlgorithmError(alg));
}

return (new Fingerprint({
        algorithm: alg,
        hash: hash,
        type: options.type || 'key',
        hashType: hashType
}));

};

function addColons(s) {

/*JSSTYLED*/
return (s.replace(/(.{2})(?=.)/g, '$1:'));

}

function base64Strip(s) {

/*JSSTYLED*/
return (s.replace(/=*$/, ''));

}

function sshBase64Format(alg, h) {

return (alg.toUpperCase() + ':' + base64Strip(h));

}

Fingerprint.isFingerprint = function (obj, ver) {

return (utils.isCompatible(obj, Fingerprint, ver));

};

/*

* API versions for Fingerprint:
* [1,0] -- initial ver
* [1,1] -- first tagged ver
* [1,2] -- hashType and spki support
*/

Fingerprint.prototype._sshpkApiVersion = [1, 2];

Fingerprint._oldVersionDetect = function (obj) {

assert.func(obj.toString);
assert.func(obj.matches);
return ([1, 0]);

};