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