// Copyright 2017 Joyent, Inc.

module.exports = Identity;

var assert = require('assert-plus'); var algs = require('./algs'); var crypto = require('crypto'); var Fingerprint = require('./fingerprint'); var Signature = require('./signature'); var errs = require('./errors'); var util = require('util'); var utils = require('./utils'); var asn1 = require('asn1'); var Buffer = require('safer-buffer').Buffer;

/JSSTYLED/ var DNS_NAME_RE = /^([*]|[a-z0-9]{0,62})(?:.([*]|[a-z0-9]{0,62}))*$/i;

var oids = {}; oids.cn = '2.5.4.3'; oids.o = '2.5.4.10'; oids.ou = '2.5.4.11'; oids.l = '2.5.4.7'; oids.s = '2.5.4.8'; oids.c = '2.5.4.6'; oids.sn = '2.5.4.4'; oids.postalCode = '2.5.4.17'; oids.serialNumber = '2.5.4.5'; oids.street = '2.5.4.9'; oids.x500UniqueIdentifier = '2.5.4.45'; oids.role = '2.5.4.72'; oids.telephoneNumber = '2.5.4.20'; oids.description = '2.5.4.13'; oids.dc = '0.9.2342.19200300.100.1.25'; oids.uid = '0.9.2342.19200300.100.1.1'; oids.mail = '0.9.2342.19200300.100.1.3'; oids.title = '2.5.4.12'; oids.gn = '2.5.4.42'; oids.initials = '2.5.4.43'; oids.pseudonym = '2.5.4.65'; oids.emailAddress = '1.2.840.113549.1.9.1';

var unoids = {}; Object.keys(oids).forEach(function (k) {

unoids[oids[k]] = k;

});

function Identity(opts) {

var self = this;
assert.object(opts, 'options');
assert.arrayOfObject(opts.components, 'options.components');
this.components = opts.components;
this.componentLookup = {};
this.components.forEach(function (c) {
        if (c.name && !c.oid)
                c.oid = oids[c.name];
        if (c.oid && !c.name)
                c.name = unoids[c.oid];
        if (self.componentLookup[c.name] === undefined)
                self.componentLookup[c.name] = [];
        self.componentLookup[c.name].push(c);
});
if (this.componentLookup.cn && this.componentLookup.cn.length > 0) {
        this.cn = this.componentLookup.cn[0].value;
}
assert.optionalString(opts.type, 'options.type');
if (opts.type === undefined) {
        if (this.components.length === 1 &&
            this.componentLookup.cn &&
            this.componentLookup.cn.length === 1 &&
            this.componentLookup.cn[0].value.match(DNS_NAME_RE)) {
                this.type = 'host';
                this.hostname = this.componentLookup.cn[0].value;

        } else if (this.componentLookup.dc &&
            this.components.length === this.componentLookup.dc.length) {
                this.type = 'host';
                this.hostname = this.componentLookup.dc.map(
                    function (c) {
                        return (c.value);
                }).join('.');

        } else if (this.componentLookup.uid &&
            this.components.length ===
            this.componentLookup.uid.length) {
                this.type = 'user';
                this.uid = this.componentLookup.uid[0].value;

        } else if (this.componentLookup.cn &&
            this.componentLookup.cn.length === 1 &&
            this.componentLookup.cn[0].value.match(DNS_NAME_RE)) {
                this.type = 'host';
                this.hostname = this.componentLookup.cn[0].value;

        } else if (this.componentLookup.uid &&
            this.componentLookup.uid.length === 1) {
                this.type = 'user';
                this.uid = this.componentLookup.uid[0].value;

        } else if (this.componentLookup.mail &&
            this.componentLookup.mail.length === 1) {
                this.type = 'email';
                this.email = this.componentLookup.mail[0].value;

        } else if (this.componentLookup.cn &&
            this.componentLookup.cn.length === 1) {
                this.type = 'user';
                this.uid = this.componentLookup.cn[0].value;

        } else {
                this.type = 'unknown';
        }
} else {
        this.type = opts.type;
        if (this.type === 'host')
                this.hostname = opts.hostname;
        else if (this.type === 'user')
                this.uid = opts.uid;
        else if (this.type === 'email')
                this.email = opts.email;
        else
                throw (new Error('Unknown type ' + this.type));
}

}

Identity.prototype.toString = function () {

return (this.components.map(function (c) {
        var n = c.name.toUpperCase();
        /*JSSTYLED*/
        n = n.replace(/=/g, '\\=');
        var v = c.value;
        /*JSSTYLED*/
        v = v.replace(/,/g, '\\,');
        return (n + '=' + v);
}).join(', '));

};

Identity.prototype.get = function (name, asArray) {

assert.string(name, 'name');
var arr = this.componentLookup[name];
if (arr === undefined || arr.length === 0)
        return (undefined);
if (!asArray && arr.length > 1)
        throw (new Error('Multiple values for attribute ' + name));
if (!asArray)
        return (arr[0].value);
return (arr.map(function (c) {
        return (c.value);
}));

};

Identity.prototype.toArray = function (idx) {

return (this.components.map(function (c) {
        return ({
                name: c.name,
                value: c.value
        });
}));

};

/*

* These are from X.680 -- PrintableString allowed chars are in section 37.4
* table 8. Spec for IA5Strings is "1,6 + SPACE + DEL" where 1 refers to
* ISO IR #001 (standard ASCII control characters) and 6 refers to ISO IR #006
* (the basic ASCII character set).
*/

/* JSSTYLED */ var NOT_PRINTABLE = /[^a-zA-Z0-9 '(),+./:=?-]/; /* JSSTYLED */ var NOT_IA5 = /[^x00-x7f]/;

Identity.prototype.toAsn1 = function (der, tag) {

der.startSequence(tag);
this.components.forEach(function (c) {
        der.startSequence(asn1.Ber.Constructor | asn1.Ber.Set);
        der.startSequence();
        der.writeOID(c.oid);
        /*
         * If we fit in a PrintableString, use that. Otherwise use an
         * IA5String or UTF8String.
         *
         * If this identity was parsed from a DN, use the ASN.1 types
         * from the original representation (otherwise this might not
         * be a full match for the original in some validators).
         */
        if (c.asn1type === asn1.Ber.Utf8String ||
            c.value.match(NOT_IA5)) {
                var v = Buffer.from(c.value, 'utf8');
                der.writeBuffer(v, asn1.Ber.Utf8String);

        } else if (c.asn1type === asn1.Ber.IA5String ||
            c.value.match(NOT_PRINTABLE)) {
                der.writeString(c.value, asn1.Ber.IA5String);

        } else {
                var type = asn1.Ber.PrintableString;
                if (c.asn1type !== undefined)
                        type = c.asn1type;
                der.writeString(c.value, type);
        }
        der.endSequence();
        der.endSequence();
});
der.endSequence();

};

function globMatch(a, b) {

if (a === '**' || b === '**')
        return (true);
var aParts = a.split('.');
var bParts = b.split('.');
if (aParts.length !== bParts.length)
        return (false);
for (var i = 0; i < aParts.length; ++i) {
        if (aParts[i] === '*' || bParts[i] === '*')
                continue;
        if (aParts[i] !== bParts[i])
                return (false);
}
return (true);

}

Identity.prototype.equals = function (other) {

if (!Identity.isIdentity(other, [1, 0]))
        return (false);
if (other.components.length !== this.components.length)
        return (false);
for (var i = 0; i < this.components.length; ++i) {
        if (this.components[i].oid !== other.components[i].oid)
                return (false);
        if (!globMatch(this.components[i].value,
            other.components[i].value)) {
                return (false);
        }
}
return (true);

};

Identity.forHost = function (hostname) {

assert.string(hostname, 'hostname');
return (new Identity({
        type: 'host',
        hostname: hostname,
        components: [ { name: 'cn', value: hostname } ]
}));

};

Identity.forUser = function (uid) {

assert.string(uid, 'uid');
return (new Identity({
        type: 'user',
        uid: uid,
        components: [ { name: 'uid', value: uid } ]
}));

};

Identity.forEmail = function (email) {

assert.string(email, 'email');
return (new Identity({
        type: 'email',
        email: email,
        components: [ { name: 'mail', value: email } ]
}));

};

Identity.parseDN = function (dn) {

assert.string(dn, 'dn');
var parts = [''];
var idx = 0;
var rem = dn;
while (rem.length > 0) {
        var m;
        /*JSSTYLED*/
        if ((m = /^,/.exec(rem)) !== null) {
                parts[++idx] = '';
                rem = rem.slice(m[0].length);
        /*JSSTYLED*/
        } else if ((m = /^\\,/.exec(rem)) !== null) {
                parts[idx] += ',';
                rem = rem.slice(m[0].length);
        /*JSSTYLED*/
        } else if ((m = /^\\./.exec(rem)) !== null) {
                parts[idx] += m[0];
                rem = rem.slice(m[0].length);
        /*JSSTYLED*/
        } else if ((m = /^[^\\,]+/.exec(rem)) !== null) {
                parts[idx] += m[0];
                rem = rem.slice(m[0].length);
        } else {
                throw (new Error('Failed to parse DN'));
        }
}
var cmps = parts.map(function (c) {
        c = c.trim();
        var eqPos = c.indexOf('=');
        while (eqPos > 0 && c.charAt(eqPos - 1) === '\\')
                eqPos = c.indexOf('=', eqPos + 1);
        if (eqPos === -1) {
                throw (new Error('Failed to parse DN'));
        }
        /*JSSTYLED*/
        var name = c.slice(0, eqPos).toLowerCase().replace(/\\=/g, '=');
        var value = c.slice(eqPos + 1);
        return ({ name: name, value: value });
});
return (new Identity({ components: cmps }));

};

Identity.fromArray = function (components) {

assert.arrayOfObject(components, 'components');
components.forEach(function (cmp) {
        assert.object(cmp, 'component');
        assert.string(cmp.name, 'component.name');
        if (!Buffer.isBuffer(cmp.value) &&
            !(typeof (cmp.value) === 'string')) {
                throw (new Error('Invalid component value'));
        }
});
return (new Identity({ components: components }));

};

Identity.parseAsn1 = function (der, top) {

var components = [];
der.readSequence(top);
var end = der.offset + der.length;
while (der.offset < end) {
        der.readSequence(asn1.Ber.Constructor | asn1.Ber.Set);
        var after = der.offset + der.length;
        der.readSequence();
        var oid = der.readOID();
        var type = der.peek();
        var value;
        switch (type) {
        case asn1.Ber.PrintableString:
        case asn1.Ber.IA5String:
        case asn1.Ber.OctetString:
        case asn1.Ber.T61String:
                value = der.readString(type);
                break;
        case asn1.Ber.Utf8String:
                value = der.readString(type, true);
                value = value.toString('utf8');
                break;
        case asn1.Ber.CharacterString:
        case asn1.Ber.BMPString:
                value = der.readString(type, true);
                value = value.toString('utf16le');
                break;
        default:
                throw (new Error('Unknown asn1 type ' + type));
        }
        components.push({ oid: oid, asn1type: type, value: value });
        der._offset = after;
}
der._offset = end;
return (new Identity({
        components: components
}));

};

Identity.isIdentity = function (obj, ver) {

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

};

/*

* API versions for Identity:
* [1,0] -- initial ver
*/

Identity.prototype._sshpkApiVersion = [1, 0];

Identity._oldVersionDetect = function (obj) {

return ([1, 0]);

};