Aleksander Machniak
2016-04-17 6e4642b12ca7a487690e4cf3e7a72fbade224d5a
commit | author | age
2965a9 1 "use strict";
TB 2
3 (function(context){
4     /*
5         Default keyservers (HTTPS and CORS enabled)
6     */
7     var DEFAULT_KEYSERVERS = [
8         "https://keys.fedoraproject.org/",
310d49 9         "https://keybase.io/"
2965a9 10     ];
TB 11
12     /*
13         Initialization to create an PublicKey object.
14
15         Arguments:
16
17         * keyservers - Array of keyserver domains, default is:
18             ["https://keys.fedoraproject.org/", "https://keybase.io/"]
19
20         Examples:
21
22         //Initialize with the default keyservers
23         var hkp = new PublicKey();
24
25         //Initialize only with a specific keyserver
26         var hkp = new PublicKey(["https://key.ip6.li/"]);
27     */
28     var PublicKey = function(keyservers){
29         this.keyservers = keyservers || DEFAULT_KEYSERVERS;
30     };
31
32     /*
33         Get a public key from any keyserver based on keyId.
34
35         Arguments:
36
37         * keyId - String key id of the public key (this is usually a fingerprint)
38
39         * callback - Function that is called when finished. Two arguments are
40                 passed to the callback: publicKey and errorCode. publicKey is
41                 an ASCII armored OpenPGP public key. errorCode is the error code
42                 (either HTTP status code or keybase error code) returned by the
43                 last keyserver that was tried. If a publicKey was found,
44                 errorCode is null. If no publicKey was found, publicKey is null
45                 and errorCode is not null.
46
47         Examples:
48
49         //Get a valid public key
50         var hkp = new PublicKey();
51         hkp.get("F75BE4E6EF6E9DD203679E94E7F6FAD172EFEE3D", function(publicKey, errorCode){
52             errorCode !== null ? console.log(errorCode) : console.log(publicKey);
53         });
54
55         //Try to get an invalid public key
56         var hkp = new PublicKey();
57         hkp.get("bogus_id", function(publicKey, errorCode){
58             errorCode !== null ? console.log(errorCode) : console.log(publicKey);
59         });
60     */
61     PublicKey.prototype.get = function(keyId, callback, keyserverIndex, err){
62         //default starting point is at the first keyserver
63         if(keyserverIndex === undefined){
64             keyserverIndex = 0;
65         }
66
67         //no more keyservers to check, so no key found
68         if(keyserverIndex >= this.keyservers.length){
69             return callback(null, err || 404);
70         }
71
72         //set the keyserver to try next
73         var ks = this.keyservers[keyserverIndex];
74         var _this = this;
75
76         //special case for keybase
77         if(ks.indexOf("https://keybase.io/") === 0){
78
79             //don't need 0x prefix for keybase searches
80             if(keyId.indexOf("0x") === 0){
81                 keyId = keyId.substr(2);
82             }
83
84             //request the public key from keybase
85             var xhr = new XMLHttpRequest();
86             xhr.open("get", "https://keybase.io/_/api/1.0/user/lookup.json" +
87                 "?fields=public_keys&key_fingerprint=" + keyId);
88             xhr.onload = function(){
89                 if(xhr.status === 200){
90                     var result = JSON.parse(xhr.responseText);
91
92                     //keybase error returns HTTP 200 status, which is silly
93                     if(result['status']['code'] !== 0){
94                         return _this.get(keyId, callback, keyserverIndex + 1, result['status']['code']);
95                     }
96
97                     //no public key found
98                     if(result['them'].length === 0){
99                         return _this.get(keyId, callback, keyserverIndex + 1, 404);
100                     }
101
102                     //found the public key
103                     var publicKey = result['them'][0]['public_keys']['primary']['bundle'];
104                     return callback(publicKey, null);
105                 }
106                 else{
107                     return _this.get(keyId, callback, keyserverIndex + 1, xhr.status);
108                 }
109             };
110             xhr.send();
111         }
112
113         //normal HKP keyserver
114         else{
115             //add the 0x prefix if absent
116             if(keyId.indexOf("0x") !== 0){
117                 keyId = "0x" + keyId;
118             }
119
120             //request the public key from the hkp server
121             var xhr = new XMLHttpRequest();
122             xhr.open("get", ks + "pks/lookup?op=get&options=mr&search=" + keyId);
123             xhr.onload = function(){
124                 if(xhr.status === 200){
125                     return callback(xhr.responseText, null);
126                 }
127                 else{
128                     return _this.get(keyId, callback, keyserverIndex + 1, xhr.status);
129                 }
130             };
131             xhr.send();
132         }
133     };
134
135     /*
136         Search for a public key in the keyservers.
137
138         Arguments:
139
140         * query - String to search for (usually an email, name, or username).
141
142         * callback - Function that is called when finished. Two arguments are
143                 passed to the callback: results and errorCode. results is an
144                 Array of users that were returned by the search. errorCode is
145                 the error code (either HTTP status code or keybase error code)
146                 returned by the last keyserver that was tried. If any results
147                 were found, errorCode is null. If no results are found, results
148                 is null and errorCode is not null.
149
150         Examples:
151
152         //Search for diafygi's key id
153         var hkp = new PublicKey();
154         hkp.search("diafygi", function(results, errorCode){
155             errorCode !== null ? console.log(errorCode) : console.log(results);
156         });
157
158         //Search for a nonexistent key id
159         var hkp = new PublicKey();
160         hkp.search("doesntexist123", function(results, errorCode){
161             errorCode !== null ? console.log(errorCode) : console.log(results);
162         });
163     */
164     PublicKey.prototype.search = function(query, callback, keyserverIndex, results, err){
165         //default starting point is at the first keyserver
166         if(keyserverIndex === undefined){
167             keyserverIndex = 0;
168         }
169
170         //initialize the results array
171         if(results === undefined){
172             results = [];
173         }
174
175         //no more keyservers to check
176         if(keyserverIndex >= this.keyservers.length){
177
178             //return error if no results
179             if(results.length === 0){
180                 return callback(null, err || 404);
181             }
182
183             //return results
184             else{
185
186                 //merge duplicates
187                 var merged = {};
188                 for(var i = 0; i < results.length; i++){
189                     var k = results[i];
190
191                     //see if there's duplicate key ids to merge
192                     if(merged[k['keyid']] !== undefined){
193
194                         for(var u = 0; u < k['uids'].length; u++){
195                             var has_this_uid = false;
196
197                             for(var m = 0; m < merged[k['keyid']]['uids'].length; m++){
198                                 if(merged[k['keyid']]['uids'][m]['uid'] === k['uids'][u]){
199                                     has_this_uid = true;
200                                     break;
201                                 }
202                             }
203
204                             if(!has_this_uid){
205                                 merged[k['keyid']]['uids'].push(k['uids'][u])
206                             }
207                         }
208                     }
209
210                     //no duplicate found, so add it to the dict
211                     else{
212                         merged[k['keyid']] = k;
213                     }
214                 }
215
216                 //return a list of the merged results in the same order
217                 var merged_list = [];
218                 for(var i = 0; i < results.length; i++){
219                     var k = results[i];
220                     if(merged[k['keyid']] !== undefined){
221                         merged_list.push(merged[k['keyid']]);
222                         delete(merged[k['keyid']]);
223                     }
224                 }
225                 return callback(merged_list, null);
226             }
227         }
228
229         //set the keyserver to try next
230         var ks = this.keyservers[keyserverIndex];
231         var _this = this;
232
233         //special case for keybase
234         if(ks.indexOf("https://keybase.io/") === 0){
235
236             //request a list of users from keybase
237             var xhr = new XMLHttpRequest();
238             xhr.open("get", "https://keybase.io/_/api/1.0/user/autocomplete.json?q=" + encodeURIComponent(query));
239             xhr.onload = function(){
240                 if(xhr.status === 200){
241                     var kb_json = JSON.parse(xhr.responseText);
242
243                     //keybase error returns HTTP 200 status, which is silly
244                     if(kb_json['status']['code'] !== 0){
245                         return _this.search(query, callback, keyserverIndex + 1, results, kb_json['status']['code']);
246                     }
247
248                     //no public key found
249                     if(kb_json['completions'].length === 0){
250                         return _this.search(query, callback, keyserverIndex + 1, results, 404);
251                     }
252
253                     //compose keybase user results
254                     var kb_results = [];
255                     for(var i = 0; i < kb_json['completions'].length; i++){
256                         var user = kb_json['completions'][i]['components'];
257
258                         //skip if no public key fingerprint
259                         if(user['key_fingerprint'] === undefined){
260                             continue;
261                         }
262
263                         //build keybase user result
264                         var kb_result = {
265                             "keyid": user['key_fingerprint']['val'].toUpperCase(),
266                             "href": "https://keybase.io/" + user['username']['val'] + "/key.asc",
267                             "info": "https://keybase.io/" + user['username']['val'],
268                             "algo": user['key_fingerprint']['algo'],
269                             "keylen": user['key_fingerprint']['nbits'],
270                             "creationdate": null,
271                             "expirationdate": null,
272                             "revoked": false,
273                             "disabled": false,
274                             "expired": false,
275                             "uids": [{
276                                 "uid": user['username']['val'] +
277                                     " on Keybase <https://keybase.io/" +
278                                     user['username']['val'] + ">",
279                                 "creationdate": null,
280                                 "expirationdate": null,
281                                 "revoked": false,
282                                 "disabled": false,
310d49 283                                 "expired": false
2965a9 284                             }]
TB 285                         };
286
287                         //add full name
288                         if(user['full_name'] !== undefined){
289                             kb_result['uids'].push({
290                                 "uid": "Full Name: " + user['full_name']['val'],
291                                 "creationdate": null,
292                                 "expirationdate": null,
293                                 "revoked": false,
294                                 "disabled": false,
310d49 295                                 "expired": false
2965a9 296                             });
TB 297                         }
298
299                         //add twitter
300                         if(user['twitter'] !== undefined){
301                             kb_result['uids'].push({
302                                 "uid": user['twitter']['val'] +
303                                     " on Twitter <https://twitter.com/" +
304                                     user['twitter']['val'] + ">",
305                                 "creationdate": null,
306                                 "expirationdate": null,
307                                 "revoked": false,
308                                 "disabled": false,
310d49 309                                 "expired": false
2965a9 310                             });
TB 311                         }
312
313                         //add github
314                         if(user['github'] !== undefined){
315                             kb_result['uids'].push({
316                                 "uid": user['github']['val'] +
317                                     " on Github <https://github.com/" +
318                                     user['github']['val'] + ">",
319                                 "creationdate": null,
320                                 "expirationdate": null,
321                                 "revoked": false,
322                                 "disabled": false,
310d49 323                                 "expired": false
2965a9 324                             });
TB 325                         }
326
327                         //add reddit
328                         if(user['reddit'] !== undefined){
329                             kb_result['uids'].push({
330                                 "uid": user['reddit']['val'] +
331                                     " on Github <https://reddit.com/u/" +
332                                     user['reddit']['val'] + ">",
333                                 "creationdate": null,
334                                 "expirationdate": null,
335                                 "revoked": false,
336                                 "disabled": false,
310d49 337                                 "expired": false
2965a9 338                             });
TB 339                         }
340
341                         //add hackernews
342                         if(user['hackernews'] !== undefined){
343                             kb_result['uids'].push({
344                                 "uid": user['hackernews']['val'] +
345                                     " on Hacker News <https://news.ycombinator.com/user?id=" +
346                                     user['hackernews']['val'] + ">",
347                                 "creationdate": null,
348                                 "expirationdate": null,
349                                 "revoked": false,
350                                 "disabled": false,
310d49 351                                 "expired": false
2965a9 352                             });
TB 353                         }
354
355                         //add coinbase
356                         if(user['coinbase'] !== undefined){
357                             kb_result['uids'].push({
358                                 "uid": user['coinbase']['val'] +
359                                     " on Coinbase <https://www.coinbase.com/" +
360                                     user['coinbase']['val'] + ">",
361                                 "creationdate": null,
362                                 "expirationdate": null,
363                                 "revoked": false,
364                                 "disabled": false,
310d49 365                                 "expired": false
2965a9 366                             });
TB 367                         }
368
369                         //add websites
370                         if(user['websites'] !== undefined){
371                             for(var w = 0; w < user['websites'].length; w++){
372                                 kb_result['uids'].push({
373                                     "uid": "Owns " + user['websites'][w]['val'],
374                                     "creationdate": null,
375                                     "expirationdate": null,
376                                     "revoked": false,
377                                     "disabled": false,
310d49 378                                     "expired": false
2965a9 379                                 });
TB 380                             }
381                         }
382
383                         kb_results.push(kb_result);
384                     }
385
386                     results = results.concat(kb_results);
387                     return _this.search(query, callback, keyserverIndex + 1, results, null);
388                 }
389                 else{
390                     return _this.search(query, callback, keyserverIndex + 1, results, xhr.status);
391                 }
392             };
393             xhr.send();
394         }
395
396         //normal HKP keyserver
397         else{
398             var xhr = new XMLHttpRequest();
399             xhr.open("get", ks + "pks/lookup?op=index&options=mr&fingerprint=on&search=" + encodeURIComponent(query));
400             xhr.onload = function(){
401                 if(xhr.status === 200){
402                     var ks_results = [];
403                     var raw = xhr.responseText.split("\n");
404                     var curKey = undefined;
405                     for(var i = 0; i < raw.length; i++){
406                         var line = raw[i].trim();
407
408                         //pub:<keyid>:<algo>:<keylen>:<creationdate>:<expirationdate>:<flags>
409                         if(line.indexOf("pub:") == 0){
410                             if(curKey !== undefined){
411                                 ks_results.push(curKey);
412                             }
413                             var vals = line.split(":");
414                             curKey = {
415                                 "keyid": vals[1],
416                                 "href": ks + "pks/lookup?op=get&options=mr&search=0x" + vals[1],
417                                 "info": ks + "pks/lookup?op=vindex&search=0x" + vals[1],
418                                 "algo": vals[2] === "" ? null : parseInt(vals[2]),
419                                 "keylen": vals[3] === "" ? null : parseInt(vals[3]),
420                                 "creationdate": vals[4] === "" ? null : parseInt(vals[4]),
421                                 "expirationdate": vals[5] === "" ? null : parseInt(vals[5]),
422                                 "revoked": vals[6].indexOf("r") !== -1,
423                                 "disabled": vals[6].indexOf("d") !== -1,
424                                 "expired": vals[6].indexOf("e") !== -1,
310d49 425                                 "uids": []
2965a9 426                             }
TB 427                         }
428
429                         //uid:<escaped uid string>:<creationdate>:<expirationdate>:<flags>
430                         if(line.indexOf("uid:") == 0){
431                             var vals = line.split(":");
432                             curKey['uids'].push({
433                                 "uid": decodeURIComponent(vals[1]),
434                                 "creationdate": vals[2] === "" ? null : parseInt(vals[2]),
435                                 "expirationdate": vals[3] === "" ? null : parseInt(vals[3]),
436                                 "revoked": vals[4].indexOf("r") !== -1,
437                                 "disabled": vals[4].indexOf("d") !== -1,
310d49 438                                 "expired": vals[4].indexOf("e") !== -1
2965a9 439                             });
TB 440                         }
441                     }
442                     ks_results.push(curKey);
443
444                     results = results.concat(ks_results);
445                     return _this.search(query, callback, keyserverIndex + 1, results, null);
446                 }
447                 else{
448                     return _this.search(query, callback, keyserverIndex + 1, results, xhr.status);
449                 }
450             };
451             xhr.send();
452         }
453     };
454
455     context.PublicKey = PublicKey;
456 })(typeof exports === "undefined" ? this : exports);