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); |