twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

identity.js (10036B)


      1 // Copyright 2017 Joyent, Inc.
      2 
      3 module.exports = Identity;
      4 
      5 var assert = require('assert-plus');
      6 var algs = require('./algs');
      7 var crypto = require('crypto');
      8 var Fingerprint = require('./fingerprint');
      9 var Signature = require('./signature');
     10 var errs = require('./errors');
     11 var util = require('util');
     12 var utils = require('./utils');
     13 var asn1 = require('asn1');
     14 var Buffer = require('safer-buffer').Buffer;
     15 
     16 /*JSSTYLED*/
     17 var DNS_NAME_RE = /^([*]|[a-z0-9][a-z0-9\-]{0,62})(?:\.([*]|[a-z0-9][a-z0-9\-]{0,62}))*$/i;
     18 
     19 var oids = {};
     20 oids.cn = '2.5.4.3';
     21 oids.o = '2.5.4.10';
     22 oids.ou = '2.5.4.11';
     23 oids.l = '2.5.4.7';
     24 oids.s = '2.5.4.8';
     25 oids.c = '2.5.4.6';
     26 oids.sn = '2.5.4.4';
     27 oids.postalCode = '2.5.4.17';
     28 oids.serialNumber = '2.5.4.5';
     29 oids.street = '2.5.4.9';
     30 oids.x500UniqueIdentifier = '2.5.4.45';
     31 oids.role = '2.5.4.72';
     32 oids.telephoneNumber = '2.5.4.20';
     33 oids.description = '2.5.4.13';
     34 oids.dc = '0.9.2342.19200300.100.1.25';
     35 oids.uid = '0.9.2342.19200300.100.1.1';
     36 oids.mail = '0.9.2342.19200300.100.1.3';
     37 oids.title = '2.5.4.12';
     38 oids.gn = '2.5.4.42';
     39 oids.initials = '2.5.4.43';
     40 oids.pseudonym = '2.5.4.65';
     41 oids.emailAddress = '1.2.840.113549.1.9.1';
     42 
     43 var unoids = {};
     44 Object.keys(oids).forEach(function (k) {
     45 	unoids[oids[k]] = k;
     46 });
     47 
     48 function Identity(opts) {
     49 	var self = this;
     50 	assert.object(opts, 'options');
     51 	assert.arrayOfObject(opts.components, 'options.components');
     52 	this.components = opts.components;
     53 	this.componentLookup = {};
     54 	this.components.forEach(function (c) {
     55 		if (c.name && !c.oid)
     56 			c.oid = oids[c.name];
     57 		if (c.oid && !c.name)
     58 			c.name = unoids[c.oid];
     59 		if (self.componentLookup[c.name] === undefined)
     60 			self.componentLookup[c.name] = [];
     61 		self.componentLookup[c.name].push(c);
     62 	});
     63 	if (this.componentLookup.cn && this.componentLookup.cn.length > 0) {
     64 		this.cn = this.componentLookup.cn[0].value;
     65 	}
     66 	assert.optionalString(opts.type, 'options.type');
     67 	if (opts.type === undefined) {
     68 		if (this.components.length === 1 &&
     69 		    this.componentLookup.cn &&
     70 		    this.componentLookup.cn.length === 1 &&
     71 		    this.componentLookup.cn[0].value.match(DNS_NAME_RE)) {
     72 			this.type = 'host';
     73 			this.hostname = this.componentLookup.cn[0].value;
     74 
     75 		} else if (this.componentLookup.dc &&
     76 		    this.components.length === this.componentLookup.dc.length) {
     77 			this.type = 'host';
     78 			this.hostname = this.componentLookup.dc.map(
     79 			    function (c) {
     80 				return (c.value);
     81 			}).join('.');
     82 
     83 		} else if (this.componentLookup.uid &&
     84 		    this.components.length ===
     85 		    this.componentLookup.uid.length) {
     86 			this.type = 'user';
     87 			this.uid = this.componentLookup.uid[0].value;
     88 
     89 		} else if (this.componentLookup.cn &&
     90 		    this.componentLookup.cn.length === 1 &&
     91 		    this.componentLookup.cn[0].value.match(DNS_NAME_RE)) {
     92 			this.type = 'host';
     93 			this.hostname = this.componentLookup.cn[0].value;
     94 
     95 		} else if (this.componentLookup.uid &&
     96 		    this.componentLookup.uid.length === 1) {
     97 			this.type = 'user';
     98 			this.uid = this.componentLookup.uid[0].value;
     99 
    100 		} else if (this.componentLookup.mail &&
    101 		    this.componentLookup.mail.length === 1) {
    102 			this.type = 'email';
    103 			this.email = this.componentLookup.mail[0].value;
    104 
    105 		} else if (this.componentLookup.cn &&
    106 		    this.componentLookup.cn.length === 1) {
    107 			this.type = 'user';
    108 			this.uid = this.componentLookup.cn[0].value;
    109 
    110 		} else {
    111 			this.type = 'unknown';
    112 		}
    113 	} else {
    114 		this.type = opts.type;
    115 		if (this.type === 'host')
    116 			this.hostname = opts.hostname;
    117 		else if (this.type === 'user')
    118 			this.uid = opts.uid;
    119 		else if (this.type === 'email')
    120 			this.email = opts.email;
    121 		else
    122 			throw (new Error('Unknown type ' + this.type));
    123 	}
    124 }
    125 
    126 Identity.prototype.toString = function () {
    127 	return (this.components.map(function (c) {
    128 		var n = c.name.toUpperCase();
    129 		/*JSSTYLED*/
    130 		n = n.replace(/=/g, '\\=');
    131 		var v = c.value;
    132 		/*JSSTYLED*/
    133 		v = v.replace(/,/g, '\\,');
    134 		return (n + '=' + v);
    135 	}).join(', '));
    136 };
    137 
    138 Identity.prototype.get = function (name, asArray) {
    139 	assert.string(name, 'name');
    140 	var arr = this.componentLookup[name];
    141 	if (arr === undefined || arr.length === 0)
    142 		return (undefined);
    143 	if (!asArray && arr.length > 1)
    144 		throw (new Error('Multiple values for attribute ' + name));
    145 	if (!asArray)
    146 		return (arr[0].value);
    147 	return (arr.map(function (c) {
    148 		return (c.value);
    149 	}));
    150 };
    151 
    152 Identity.prototype.toArray = function (idx) {
    153 	return (this.components.map(function (c) {
    154 		return ({
    155 			name: c.name,
    156 			value: c.value
    157 		});
    158 	}));
    159 };
    160 
    161 /*
    162  * These are from X.680 -- PrintableString allowed chars are in section 37.4
    163  * table 8. Spec for IA5Strings is "1,6 + SPACE + DEL" where 1 refers to
    164  * ISO IR #001 (standard ASCII control characters) and 6 refers to ISO IR #006
    165  * (the basic ASCII character set).
    166  */
    167 /* JSSTYLED */
    168 var NOT_PRINTABLE = /[^a-zA-Z0-9 '(),+.\/:=?-]/;
    169 /* JSSTYLED */
    170 var NOT_IA5 = /[^\x00-\x7f]/;
    171 
    172 Identity.prototype.toAsn1 = function (der, tag) {
    173 	der.startSequence(tag);
    174 	this.components.forEach(function (c) {
    175 		der.startSequence(asn1.Ber.Constructor | asn1.Ber.Set);
    176 		der.startSequence();
    177 		der.writeOID(c.oid);
    178 		/*
    179 		 * If we fit in a PrintableString, use that. Otherwise use an
    180 		 * IA5String or UTF8String.
    181 		 *
    182 		 * If this identity was parsed from a DN, use the ASN.1 types
    183 		 * from the original representation (otherwise this might not
    184 		 * be a full match for the original in some validators).
    185 		 */
    186 		if (c.asn1type === asn1.Ber.Utf8String ||
    187 		    c.value.match(NOT_IA5)) {
    188 			var v = Buffer.from(c.value, 'utf8');
    189 			der.writeBuffer(v, asn1.Ber.Utf8String);
    190 
    191 		} else if (c.asn1type === asn1.Ber.IA5String ||
    192 		    c.value.match(NOT_PRINTABLE)) {
    193 			der.writeString(c.value, asn1.Ber.IA5String);
    194 
    195 		} else {
    196 			var type = asn1.Ber.PrintableString;
    197 			if (c.asn1type !== undefined)
    198 				type = c.asn1type;
    199 			der.writeString(c.value, type);
    200 		}
    201 		der.endSequence();
    202 		der.endSequence();
    203 	});
    204 	der.endSequence();
    205 };
    206 
    207 function globMatch(a, b) {
    208 	if (a === '**' || b === '**')
    209 		return (true);
    210 	var aParts = a.split('.');
    211 	var bParts = b.split('.');
    212 	if (aParts.length !== bParts.length)
    213 		return (false);
    214 	for (var i = 0; i < aParts.length; ++i) {
    215 		if (aParts[i] === '*' || bParts[i] === '*')
    216 			continue;
    217 		if (aParts[i] !== bParts[i])
    218 			return (false);
    219 	}
    220 	return (true);
    221 }
    222 
    223 Identity.prototype.equals = function (other) {
    224 	if (!Identity.isIdentity(other, [1, 0]))
    225 		return (false);
    226 	if (other.components.length !== this.components.length)
    227 		return (false);
    228 	for (var i = 0; i < this.components.length; ++i) {
    229 		if (this.components[i].oid !== other.components[i].oid)
    230 			return (false);
    231 		if (!globMatch(this.components[i].value,
    232 		    other.components[i].value)) {
    233 			return (false);
    234 		}
    235 	}
    236 	return (true);
    237 };
    238 
    239 Identity.forHost = function (hostname) {
    240 	assert.string(hostname, 'hostname');
    241 	return (new Identity({
    242 		type: 'host',
    243 		hostname: hostname,
    244 		components: [ { name: 'cn', value: hostname } ]
    245 	}));
    246 };
    247 
    248 Identity.forUser = function (uid) {
    249 	assert.string(uid, 'uid');
    250 	return (new Identity({
    251 		type: 'user',
    252 		uid: uid,
    253 		components: [ { name: 'uid', value: uid } ]
    254 	}));
    255 };
    256 
    257 Identity.forEmail = function (email) {
    258 	assert.string(email, 'email');
    259 	return (new Identity({
    260 		type: 'email',
    261 		email: email,
    262 		components: [ { name: 'mail', value: email } ]
    263 	}));
    264 };
    265 
    266 Identity.parseDN = function (dn) {
    267 	assert.string(dn, 'dn');
    268 	var parts = [''];
    269 	var idx = 0;
    270 	var rem = dn;
    271 	while (rem.length > 0) {
    272 		var m;
    273 		/*JSSTYLED*/
    274 		if ((m = /^,/.exec(rem)) !== null) {
    275 			parts[++idx] = '';
    276 			rem = rem.slice(m[0].length);
    277 		/*JSSTYLED*/
    278 		} else if ((m = /^\\,/.exec(rem)) !== null) {
    279 			parts[idx] += ',';
    280 			rem = rem.slice(m[0].length);
    281 		/*JSSTYLED*/
    282 		} else if ((m = /^\\./.exec(rem)) !== null) {
    283 			parts[idx] += m[0];
    284 			rem = rem.slice(m[0].length);
    285 		/*JSSTYLED*/
    286 		} else if ((m = /^[^\\,]+/.exec(rem)) !== null) {
    287 			parts[idx] += m[0];
    288 			rem = rem.slice(m[0].length);
    289 		} else {
    290 			throw (new Error('Failed to parse DN'));
    291 		}
    292 	}
    293 	var cmps = parts.map(function (c) {
    294 		c = c.trim();
    295 		var eqPos = c.indexOf('=');
    296 		while (eqPos > 0 && c.charAt(eqPos - 1) === '\\')
    297 			eqPos = c.indexOf('=', eqPos + 1);
    298 		if (eqPos === -1) {
    299 			throw (new Error('Failed to parse DN'));
    300 		}
    301 		/*JSSTYLED*/
    302 		var name = c.slice(0, eqPos).toLowerCase().replace(/\\=/g, '=');
    303 		var value = c.slice(eqPos + 1);
    304 		return ({ name: name, value: value });
    305 	});
    306 	return (new Identity({ components: cmps }));
    307 };
    308 
    309 Identity.fromArray = function (components) {
    310 	assert.arrayOfObject(components, 'components');
    311 	components.forEach(function (cmp) {
    312 		assert.object(cmp, 'component');
    313 		assert.string(cmp.name, 'component.name');
    314 		if (!Buffer.isBuffer(cmp.value) &&
    315 		    !(typeof (cmp.value) === 'string')) {
    316 			throw (new Error('Invalid component value'));
    317 		}
    318 	});
    319 	return (new Identity({ components: components }));
    320 };
    321 
    322 Identity.parseAsn1 = function (der, top) {
    323 	var components = [];
    324 	der.readSequence(top);
    325 	var end = der.offset + der.length;
    326 	while (der.offset < end) {
    327 		der.readSequence(asn1.Ber.Constructor | asn1.Ber.Set);
    328 		var after = der.offset + der.length;
    329 		der.readSequence();
    330 		var oid = der.readOID();
    331 		var type = der.peek();
    332 		var value;
    333 		switch (type) {
    334 		case asn1.Ber.PrintableString:
    335 		case asn1.Ber.IA5String:
    336 		case asn1.Ber.OctetString:
    337 		case asn1.Ber.T61String:
    338 			value = der.readString(type);
    339 			break;
    340 		case asn1.Ber.Utf8String:
    341 			value = der.readString(type, true);
    342 			value = value.toString('utf8');
    343 			break;
    344 		case asn1.Ber.CharacterString:
    345 		case asn1.Ber.BMPString:
    346 			value = der.readString(type, true);
    347 			value = value.toString('utf16le');
    348 			break;
    349 		default:
    350 			throw (new Error('Unknown asn1 type ' + type));
    351 		}
    352 		components.push({ oid: oid, asn1type: type, value: value });
    353 		der._offset = after;
    354 	}
    355 	der._offset = end;
    356 	return (new Identity({
    357 		components: components
    358 	}));
    359 };
    360 
    361 Identity.isIdentity = function (obj, ver) {
    362 	return (utils.isCompatible(obj, Identity, ver));
    363 };
    364 
    365 /*
    366  * API versions for Identity:
    367  * [1,0] -- initial ver
    368  */
    369 Identity.prototype._sshpkApiVersion = [1, 0];
    370 
    371 Identity._oldVersionDetect = function (obj) {
    372 	return ([1, 0]);
    373 };