0
|
1 "use strict";
|
|
2
|
|
3 (function(context){
|
|
4 /*
|
|
5 Default keyservers (HTTPS and CORS enabled)
|
|
6 */
|
|
7 var DEFAULT_KEYSERVERS = [
|
|
8 "https://keys.fedoraproject.org/",
|
|
9 "https://keybase.io/",
|
|
10 ];
|
|
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,
|
|
283 "expired": false,
|
|
284 }]
|
|
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,
|
|
295 "expired": false,
|
|
296 });
|
|
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,
|
|
309 "expired": false,
|
|
310 });
|
|
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,
|
|
323 "expired": false,
|
|
324 });
|
|
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,
|
|
337 "expired": false,
|
|
338 });
|
|
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,
|
|
351 "expired": false,
|
|
352 });
|
|
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,
|
|
365 "expired": false,
|
|
366 });
|
|
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,
|
|
378 "expired": false,
|
|
379 });
|
|
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,
|
|
425 "uids": [],
|
|
426 }
|
|
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,
|
|
438 "expired": vals[4].indexOf("e") !== -1,
|
|
439 });
|
|
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);
|