New file |
| | |
| | | "use strict"; |
| | | |
| | | (function(context){ |
| | | /* |
| | | Default keyservers (HTTPS and CORS enabled) |
| | | */ |
| | | var DEFAULT_KEYSERVERS = [ |
| | | "https://keys.fedoraproject.org/", |
| | | "https://keybase.io/", |
| | | ]; |
| | | |
| | | /* |
| | | Initialization to create an PublicKey object. |
| | | |
| | | Arguments: |
| | | |
| | | * keyservers - Array of keyserver domains, default is: |
| | | ["https://keys.fedoraproject.org/", "https://keybase.io/"] |
| | | |
| | | Examples: |
| | | |
| | | //Initialize with the default keyservers |
| | | var hkp = new PublicKey(); |
| | | |
| | | //Initialize only with a specific keyserver |
| | | var hkp = new PublicKey(["https://key.ip6.li/"]); |
| | | */ |
| | | var PublicKey = function(keyservers){ |
| | | this.keyservers = keyservers || DEFAULT_KEYSERVERS; |
| | | }; |
| | | |
| | | /* |
| | | Get a public key from any keyserver based on keyId. |
| | | |
| | | Arguments: |
| | | |
| | | * keyId - String key id of the public key (this is usually a fingerprint) |
| | | |
| | | * callback - Function that is called when finished. Two arguments are |
| | | passed to the callback: publicKey and errorCode. publicKey is |
| | | an ASCII armored OpenPGP public key. errorCode is the error code |
| | | (either HTTP status code or keybase error code) returned by the |
| | | last keyserver that was tried. If a publicKey was found, |
| | | errorCode is null. If no publicKey was found, publicKey is null |
| | | and errorCode is not null. |
| | | |
| | | Examples: |
| | | |
| | | //Get a valid public key |
| | | var hkp = new PublicKey(); |
| | | hkp.get("F75BE4E6EF6E9DD203679E94E7F6FAD172EFEE3D", function(publicKey, errorCode){ |
| | | errorCode !== null ? console.log(errorCode) : console.log(publicKey); |
| | | }); |
| | | |
| | | //Try to get an invalid public key |
| | | var hkp = new PublicKey(); |
| | | hkp.get("bogus_id", function(publicKey, errorCode){ |
| | | errorCode !== null ? console.log(errorCode) : console.log(publicKey); |
| | | }); |
| | | */ |
| | | PublicKey.prototype.get = function(keyId, callback, keyserverIndex, err){ |
| | | //default starting point is at the first keyserver |
| | | if(keyserverIndex === undefined){ |
| | | keyserverIndex = 0; |
| | | } |
| | | |
| | | //no more keyservers to check, so no key found |
| | | if(keyserverIndex >= this.keyservers.length){ |
| | | return callback(null, err || 404); |
| | | } |
| | | |
| | | //set the keyserver to try next |
| | | var ks = this.keyservers[keyserverIndex]; |
| | | var _this = this; |
| | | |
| | | //special case for keybase |
| | | if(ks.indexOf("https://keybase.io/") === 0){ |
| | | |
| | | //don't need 0x prefix for keybase searches |
| | | if(keyId.indexOf("0x") === 0){ |
| | | keyId = keyId.substr(2); |
| | | } |
| | | |
| | | //request the public key from keybase |
| | | var xhr = new XMLHttpRequest(); |
| | | xhr.open("get", "https://keybase.io/_/api/1.0/user/lookup.json" + |
| | | "?fields=public_keys&key_fingerprint=" + keyId); |
| | | xhr.onload = function(){ |
| | | if(xhr.status === 200){ |
| | | var result = JSON.parse(xhr.responseText); |
| | | |
| | | //keybase error returns HTTP 200 status, which is silly |
| | | if(result['status']['code'] !== 0){ |
| | | return _this.get(keyId, callback, keyserverIndex + 1, result['status']['code']); |
| | | } |
| | | |
| | | //no public key found |
| | | if(result['them'].length === 0){ |
| | | return _this.get(keyId, callback, keyserverIndex + 1, 404); |
| | | } |
| | | |
| | | //found the public key |
| | | var publicKey = result['them'][0]['public_keys']['primary']['bundle']; |
| | | return callback(publicKey, null); |
| | | } |
| | | else{ |
| | | return _this.get(keyId, callback, keyserverIndex + 1, xhr.status); |
| | | } |
| | | }; |
| | | xhr.send(); |
| | | } |
| | | |
| | | //normal HKP keyserver |
| | | else{ |
| | | //add the 0x prefix if absent |
| | | if(keyId.indexOf("0x") !== 0){ |
| | | keyId = "0x" + keyId; |
| | | } |
| | | |
| | | //request the public key from the hkp server |
| | | var xhr = new XMLHttpRequest(); |
| | | xhr.open("get", ks + "pks/lookup?op=get&options=mr&search=" + keyId); |
| | | xhr.onload = function(){ |
| | | if(xhr.status === 200){ |
| | | return callback(xhr.responseText, null); |
| | | } |
| | | else{ |
| | | return _this.get(keyId, callback, keyserverIndex + 1, xhr.status); |
| | | } |
| | | }; |
| | | xhr.send(); |
| | | } |
| | | }; |
| | | |
| | | /* |
| | | Search for a public key in the keyservers. |
| | | |
| | | Arguments: |
| | | |
| | | * query - String to search for (usually an email, name, or username). |
| | | |
| | | * callback - Function that is called when finished. Two arguments are |
| | | passed to the callback: results and errorCode. results is an |
| | | Array of users that were returned by the search. errorCode is |
| | | the error code (either HTTP status code or keybase error code) |
| | | returned by the last keyserver that was tried. If any results |
| | | were found, errorCode is null. If no results are found, results |
| | | is null and errorCode is not null. |
| | | |
| | | Examples: |
| | | |
| | | //Search for diafygi's key id |
| | | var hkp = new PublicKey(); |
| | | hkp.search("diafygi", function(results, errorCode){ |
| | | errorCode !== null ? console.log(errorCode) : console.log(results); |
| | | }); |
| | | |
| | | //Search for a nonexistent key id |
| | | var hkp = new PublicKey(); |
| | | hkp.search("doesntexist123", function(results, errorCode){ |
| | | errorCode !== null ? console.log(errorCode) : console.log(results); |
| | | }); |
| | | */ |
| | | PublicKey.prototype.search = function(query, callback, keyserverIndex, results, err){ |
| | | //default starting point is at the first keyserver |
| | | if(keyserverIndex === undefined){ |
| | | keyserverIndex = 0; |
| | | } |
| | | |
| | | //initialize the results array |
| | | if(results === undefined){ |
| | | results = []; |
| | | } |
| | | |
| | | //no more keyservers to check |
| | | if(keyserverIndex >= this.keyservers.length){ |
| | | |
| | | //return error if no results |
| | | if(results.length === 0){ |
| | | return callback(null, err || 404); |
| | | } |
| | | |
| | | //return results |
| | | else{ |
| | | |
| | | //merge duplicates |
| | | var merged = {}; |
| | | for(var i = 0; i < results.length; i++){ |
| | | var k = results[i]; |
| | | |
| | | //see if there's duplicate key ids to merge |
| | | if(merged[k['keyid']] !== undefined){ |
| | | |
| | | for(var u = 0; u < k['uids'].length; u++){ |
| | | var has_this_uid = false; |
| | | |
| | | for(var m = 0; m < merged[k['keyid']]['uids'].length; m++){ |
| | | if(merged[k['keyid']]['uids'][m]['uid'] === k['uids'][u]){ |
| | | has_this_uid = true; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if(!has_this_uid){ |
| | | merged[k['keyid']]['uids'].push(k['uids'][u]) |
| | | } |
| | | } |
| | | } |
| | | |
| | | //no duplicate found, so add it to the dict |
| | | else{ |
| | | merged[k['keyid']] = k; |
| | | } |
| | | } |
| | | |
| | | //return a list of the merged results in the same order |
| | | var merged_list = []; |
| | | for(var i = 0; i < results.length; i++){ |
| | | var k = results[i]; |
| | | if(merged[k['keyid']] !== undefined){ |
| | | merged_list.push(merged[k['keyid']]); |
| | | delete(merged[k['keyid']]); |
| | | } |
| | | } |
| | | return callback(merged_list, null); |
| | | } |
| | | } |
| | | |
| | | //set the keyserver to try next |
| | | var ks = this.keyservers[keyserverIndex]; |
| | | var _this = this; |
| | | |
| | | //special case for keybase |
| | | if(ks.indexOf("https://keybase.io/") === 0){ |
| | | |
| | | //request a list of users from keybase |
| | | var xhr = new XMLHttpRequest(); |
| | | xhr.open("get", "https://keybase.io/_/api/1.0/user/autocomplete.json?q=" + encodeURIComponent(query)); |
| | | xhr.onload = function(){ |
| | | if(xhr.status === 200){ |
| | | var kb_json = JSON.parse(xhr.responseText); |
| | | |
| | | //keybase error returns HTTP 200 status, which is silly |
| | | if(kb_json['status']['code'] !== 0){ |
| | | return _this.search(query, callback, keyserverIndex + 1, results, kb_json['status']['code']); |
| | | } |
| | | |
| | | //no public key found |
| | | if(kb_json['completions'].length === 0){ |
| | | return _this.search(query, callback, keyserverIndex + 1, results, 404); |
| | | } |
| | | |
| | | //compose keybase user results |
| | | var kb_results = []; |
| | | for(var i = 0; i < kb_json['completions'].length; i++){ |
| | | var user = kb_json['completions'][i]['components']; |
| | | |
| | | //skip if no public key fingerprint |
| | | if(user['key_fingerprint'] === undefined){ |
| | | continue; |
| | | } |
| | | |
| | | //build keybase user result |
| | | var kb_result = { |
| | | "keyid": user['key_fingerprint']['val'].toUpperCase(), |
| | | "href": "https://keybase.io/" + user['username']['val'] + "/key.asc", |
| | | "info": "https://keybase.io/" + user['username']['val'], |
| | | "algo": user['key_fingerprint']['algo'], |
| | | "keylen": user['key_fingerprint']['nbits'], |
| | | "creationdate": null, |
| | | "expirationdate": null, |
| | | "revoked": false, |
| | | "disabled": false, |
| | | "expired": false, |
| | | "uids": [{ |
| | | "uid": user['username']['val'] + |
| | | " on Keybase <https://keybase.io/" + |
| | | user['username']['val'] + ">", |
| | | "creationdate": null, |
| | | "expirationdate": null, |
| | | "revoked": false, |
| | | "disabled": false, |
| | | "expired": false, |
| | | }] |
| | | }; |
| | | |
| | | //add full name |
| | | if(user['full_name'] !== undefined){ |
| | | kb_result['uids'].push({ |
| | | "uid": "Full Name: " + user['full_name']['val'], |
| | | "creationdate": null, |
| | | "expirationdate": null, |
| | | "revoked": false, |
| | | "disabled": false, |
| | | "expired": false, |
| | | }); |
| | | } |
| | | |
| | | //add twitter |
| | | if(user['twitter'] !== undefined){ |
| | | kb_result['uids'].push({ |
| | | "uid": user['twitter']['val'] + |
| | | " on Twitter <https://twitter.com/" + |
| | | user['twitter']['val'] + ">", |
| | | "creationdate": null, |
| | | "expirationdate": null, |
| | | "revoked": false, |
| | | "disabled": false, |
| | | "expired": false, |
| | | }); |
| | | } |
| | | |
| | | //add github |
| | | if(user['github'] !== undefined){ |
| | | kb_result['uids'].push({ |
| | | "uid": user['github']['val'] + |
| | | " on Github <https://github.com/" + |
| | | user['github']['val'] + ">", |
| | | "creationdate": null, |
| | | "expirationdate": null, |
| | | "revoked": false, |
| | | "disabled": false, |
| | | "expired": false, |
| | | }); |
| | | } |
| | | |
| | | //add reddit |
| | | if(user['reddit'] !== undefined){ |
| | | kb_result['uids'].push({ |
| | | "uid": user['reddit']['val'] + |
| | | " on Github <https://reddit.com/u/" + |
| | | user['reddit']['val'] + ">", |
| | | "creationdate": null, |
| | | "expirationdate": null, |
| | | "revoked": false, |
| | | "disabled": false, |
| | | "expired": false, |
| | | }); |
| | | } |
| | | |
| | | //add hackernews |
| | | if(user['hackernews'] !== undefined){ |
| | | kb_result['uids'].push({ |
| | | "uid": user['hackernews']['val'] + |
| | | " on Hacker News <https://news.ycombinator.com/user?id=" + |
| | | user['hackernews']['val'] + ">", |
| | | "creationdate": null, |
| | | "expirationdate": null, |
| | | "revoked": false, |
| | | "disabled": false, |
| | | "expired": false, |
| | | }); |
| | | } |
| | | |
| | | //add coinbase |
| | | if(user['coinbase'] !== undefined){ |
| | | kb_result['uids'].push({ |
| | | "uid": user['coinbase']['val'] + |
| | | " on Coinbase <https://www.coinbase.com/" + |
| | | user['coinbase']['val'] + ">", |
| | | "creationdate": null, |
| | | "expirationdate": null, |
| | | "revoked": false, |
| | | "disabled": false, |
| | | "expired": false, |
| | | }); |
| | | } |
| | | |
| | | //add websites |
| | | if(user['websites'] !== undefined){ |
| | | for(var w = 0; w < user['websites'].length; w++){ |
| | | kb_result['uids'].push({ |
| | | "uid": "Owns " + user['websites'][w]['val'], |
| | | "creationdate": null, |
| | | "expirationdate": null, |
| | | "revoked": false, |
| | | "disabled": false, |
| | | "expired": false, |
| | | }); |
| | | } |
| | | } |
| | | |
| | | kb_results.push(kb_result); |
| | | } |
| | | |
| | | results = results.concat(kb_results); |
| | | return _this.search(query, callback, keyserverIndex + 1, results, null); |
| | | } |
| | | else{ |
| | | return _this.search(query, callback, keyserverIndex + 1, results, xhr.status); |
| | | } |
| | | }; |
| | | xhr.send(); |
| | | } |
| | | |
| | | //normal HKP keyserver |
| | | else{ |
| | | var xhr = new XMLHttpRequest(); |
| | | xhr.open("get", ks + "pks/lookup?op=index&options=mr&fingerprint=on&search=" + encodeURIComponent(query)); |
| | | xhr.onload = function(){ |
| | | if(xhr.status === 200){ |
| | | var ks_results = []; |
| | | var raw = xhr.responseText.split("\n"); |
| | | var curKey = undefined; |
| | | for(var i = 0; i < raw.length; i++){ |
| | | var line = raw[i].trim(); |
| | | |
| | | //pub:<keyid>:<algo>:<keylen>:<creationdate>:<expirationdate>:<flags> |
| | | if(line.indexOf("pub:") == 0){ |
| | | if(curKey !== undefined){ |
| | | ks_results.push(curKey); |
| | | } |
| | | var vals = line.split(":"); |
| | | curKey = { |
| | | "keyid": vals[1], |
| | | "href": ks + "pks/lookup?op=get&options=mr&search=0x" + vals[1], |
| | | "info": ks + "pks/lookup?op=vindex&search=0x" + vals[1], |
| | | "algo": vals[2] === "" ? null : parseInt(vals[2]), |
| | | "keylen": vals[3] === "" ? null : parseInt(vals[3]), |
| | | "creationdate": vals[4] === "" ? null : parseInt(vals[4]), |
| | | "expirationdate": vals[5] === "" ? null : parseInt(vals[5]), |
| | | "revoked": vals[6].indexOf("r") !== -1, |
| | | "disabled": vals[6].indexOf("d") !== -1, |
| | | "expired": vals[6].indexOf("e") !== -1, |
| | | "uids": [], |
| | | } |
| | | } |
| | | |
| | | //uid:<escaped uid string>:<creationdate>:<expirationdate>:<flags> |
| | | if(line.indexOf("uid:") == 0){ |
| | | var vals = line.split(":"); |
| | | curKey['uids'].push({ |
| | | "uid": decodeURIComponent(vals[1]), |
| | | "creationdate": vals[2] === "" ? null : parseInt(vals[2]), |
| | | "expirationdate": vals[3] === "" ? null : parseInt(vals[3]), |
| | | "revoked": vals[4].indexOf("r") !== -1, |
| | | "disabled": vals[4].indexOf("d") !== -1, |
| | | "expired": vals[4].indexOf("e") !== -1, |
| | | }); |
| | | } |
| | | } |
| | | ks_results.push(curKey); |
| | | |
| | | results = results.concat(ks_results); |
| | | return _this.search(query, callback, keyserverIndex + 1, results, null); |
| | | } |
| | | else{ |
| | | return _this.search(query, callback, keyserverIndex + 1, results, xhr.status); |
| | | } |
| | | }; |
| | | xhr.send(); |
| | | } |
| | | }; |
| | | |
| | | context.PublicKey = PublicKey; |
| | | })(typeof exports === "undefined" ? this : exports); |