| // 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][a-z0-9\-]{0,62})(?:\.([*]|[a-z0-9][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]); | 
| }; |