/* A JavaScript implementation of the SHA family of hashes, as defined in FIPS PUB 180-4 and FIPS PUB 202, as well as the corresponding HMAC implementation as defined in FIPS PUB 198a Copyright Brian Turek 2008-2017 Distributed under the BSD License See http://caligatio.github.com/jsSHA/ for more information Several functions taken from Paul Johnston */ 'use strict';(function(I){function w(c,a,d){var l=0,b=[],g=0,f,n,k,e,h,q,y,p,m=!1,t=[],r=[],u,z=!1;d=d||{};f=d.encoding||"UTF8";u=d.numRounds||1;if(u!==parseInt(u,10)||1>u)throw Error("numRounds must a integer >= 1");if(0===c.lastIndexOf("SHA-",0))if(q=function(b,a){return A(b,a,c)},y=function(b,a,l,f){var g,e;if("SHA-224"===c||"SHA-256"===c)g=(a+65>>>9<<4)+15,e=16;else throw Error("Unexpected error in SHA-2 implementation");for(;b.length<=g;)b.push(0);b[a>>>5]|=128<<24-a%32;a=a+l;b[g]=a&4294967295; b[g-1]=a/4294967296|0;l=b.length;for(a=0;a>>3;g=e/4-1;if(eb/8){for(;a.length<=g;)a.push(0);a[g]&=4294967040}for(b=0;b<=g;b+=1)t[b]=a[b]^909522486,r[b]=a[b]^1549556828;n=q(t,n);l=h;m=!0};this.update=function(a){var c,f,e,d=0,p=h>>>5;c=k(a,b,g);a=c.binLen;f=c.value;c=a>>>5;for(e=0;e>> 5);g=a%h;z=!0};this.getHash=function(a,f){var d,h,k,q;if(!0===m)throw Error("Cannot call getHash after setting HMAC key");k=C(f);switch(a){case "HEX":d=function(a){return D(a,e,k)};break;case "B64":d=function(a){return E(a,e,k)};break;case "BYTES":d=function(a){return F(a,e)};break;case "ARRAYBUFFER":try{h=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a){return G(a,e)};break;default:throw Error("format must be HEX, B64, BYTES, or ARRAYBUFFER"); }q=y(b.slice(),g,l,p(n));for(h=1;h>>2]>>>8*(3+b%4*-1),l+="0123456789abcdef".charAt(g>>>4&15)+"0123456789abcdef".charAt(g&15);return d.outputUpper?l.toUpperCase():l}function E(c,a,d){var l="",b=a/8,g,f,n;for(g=0;g>>2]:0,n=g+2>>2]:0,n=(c[g>>>2]>>>8*(3+g%4*-1)&255)<<16|(f>>>8*(3+(g+1)%4*-1)&255)<<8|n>>>8*(3+(g+2)%4*-1)&255,f=0;4>f;f+=1)8*g+6*f<=a?l+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(n>>> 6*(3-f)&63):l+=d.b64Pad;return l}function F(c,a){var d="",l=a/8,b,g;for(b=0;b>>2]>>>8*(3+b%4*-1)&255,d+=String.fromCharCode(g);return d}function G(c,a){var d=a/8,l,b=new ArrayBuffer(d),g;g=new Uint8Array(b);for(l=0;l>>2]>>>8*(3+l%4*-1)&255;return b}function C(c){var a={outputUpper:!1,b64Pad:"=",shakeLen:-1};c=c||{};a.outputUpper=c.outputUpper||!1;!0===c.hasOwnProperty("b64Pad")&&(a.b64Pad=c.b64Pad);if("boolean"!==typeof a.outputUpper)throw Error("Invalid outputUpper formatting option"); if("string"!==typeof a.b64Pad)throw Error("Invalid b64Pad formatting option");return a}function B(c,a){var d;switch(a){case "UTF8":case "UTF16BE":case "UTF16LE":break;default:throw Error("encoding must be UTF8, UTF16BE, or UTF16LE");}switch(c){case "HEX":d=function(a,b,c){var f=a.length,d,k,e,h,q;if(0!==f%2)throw Error("String of HEX type must be in byte increments");b=b||[0];c=c||0;q=c>>>3;for(d=0;d>>1)+q;for(e=h>>>2;b.length<=e;)b.push(0);b[e]|=k<<8*(3+h%4*-1)}return{value:b,binLen:4*f+c}};break;case "TEXT":d=function(c,b,d){var f,n,k=0,e,h,q,m,p,r;b=b||[0];d=d||0;q=d>>>3;if("UTF8"===a)for(r=3,e=0;ef?n.push(f):2048>f?(n.push(192|f>>>6),n.push(128|f&63)):55296>f||57344<=f?n.push(224|f>>>12,128|f>>>6&63,128|f&63):(e+=1,f=65536+((f&1023)<<10|c.charCodeAt(e)&1023),n.push(240|f>>>18,128|f>>>12&63,128|f>>>6&63,128|f&63)),h=0;h>>2;b.length<=m;)b.push(0);b[m]|=n[h]<<8*(r+p%4*-1);k+=1}else if("UTF16BE"===a||"UTF16LE"===a)for(r=2,n="UTF16LE"===a&&!0||"UTF16LE"!==a&&!1,e=0;e>>8);p=k+q;for(m=p>>>2;b.length<=m;)b.push(0);b[m]|=f<<8*(r+p%4*-1);k+=2}return{value:b,binLen:8*k+d}};break;case "B64":d=function(a,b,c){var f=0,d,k,e,h,q,m,p;if(-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw Error("Invalid character in base-64 string");k=a.indexOf("=");a=a.replace(/\=/g, "");if(-1!==k&&k { var new_blocked_data; // Make sure the entry in unused_data exists var url = blocked_info["url"]; if(url === undefined){ console.error("No url passed to update_popup"); return 1; } if(unused_data[tab_id] === undefined){ unused_data[tab_id] = { "accepted":[], "blocked":[], "blacklisted":[], "whitelisted":[], "url": url }; } if(unused_data[tab_id]["accepted"] === undefined){unused_data[tab_id]["accepted"] = [];} if(unused_data[tab_id]["blocked"] === undefined){unused_data[tab_id]["blocked"] = [];} if(unused_data[tab_id]["blacklisted"] === undefined){unused_data[tab_id]["blacklisted"] = [];} if(unused_data[tab_id]["whitelisted"] === undefined){unused_data[tab_id]["whitelisted"] = [];} var type = ""; if(blocked_info["accepted"] !== undefined){ type = "accepted"; } if(blocked_info["blocked"] !== undefined){ type = "blocked"; } function get_sto(items){ function get_status(script_name,src_hash){ var script_key = get_storage_key(script_name,src_hash); if(items[script_key] === undefined){ return "none"; } return items[script_key]; } function is_bl(script_name){ if(get_status(script_name) == "blacklist"){ return true; } return false; } function is_wl(script_name){ if(get_status(script_name) == "whitelist"){ return true; } return false; } // Search unused data for the given entry function not_duplicate(entry,key){ var flag = true; for(var i = 0; i < unused_data[tab_id][entry].length; i++){ if(unused_data[tab_id][entry][i][0] == key[0]){ flag = false; } } return flag; } var type_key = ""; var res = ""; if(is_bl(blocked_info[type][0])){ type_key = "blacklisted"; res = "bl"; //console.log("Script " + blocked_info[type][0] + " is blacklisted"); } else if(is_wl(blocked_info[type][0])){ type_key = "whitelisted"; res = "wl"; //console.log("Script " + blocked_info[type][0] + " is whitelisted"); } else{ type_key = type; res = "none"; //console.log("Script " + blocked_info[type][0] + " isn't whitelisted or blacklisted"); } if(not_duplicate(type_key,blocked_info[type])){ unused_data[tab_id][type_key].push(blocked_info[type]); resolve(res); } else{ resolve(res); } } webex.storage.local.get(get_sto); }); } function get_domain(url){ var domain = url.replace('http://','').replace('https://','').split(/[/?#]/)[0]; if(url.indexOf("http://") == 0){ domain = "http://" + domain; } else if(url.indexOf("https://") == 0){ domain = "https://" + domain; } domain = domain + "/"; domain = domain.replace(/ /g,""); return domain; } /** * * This is the callback where the content scripts of the browser action will contact the background script. * */ var portFromCS; function connected(p) { if(p["name"] == "contact_finder"){ // Send a message back with the relevant settings function cb(items){ p.postMessage(items); } webex.storage.local.get(cb); return; } p.onMessage.addListener(function(m) { /** * Updates the entry of the current URL in storage */ function set_script(script,val){ if(val != "whitelist" && val != "forget" && val != "blacklist"){ console.error("Key must be either 'whitelist', 'blacklist' or 'forget'"); } // (Remember that we do not trust the names of scripts.) var current_url = ""; function geturl(tabs) { current_url = tabs[0]["url"]; var domain = get_domain(current_url); // The space char is a valid delimiter because encodeURI() replaces it with %20 var scriptkey = m[val][0]; if(val == "forget"){ var prom = webex.storage.local.remove(scriptkey); // TODO: This should produce a "Refresh the page for this change to take effect" message } else{ var newitem = {}; newitem[scriptkey] = val; webex.storage.local.set(newitem); } } var querying = webex.tabs.query({active: true,currentWindow: true},geturl); return; } var update = false; var contact_finder = false; if(m["whitelist"] !== undefined){ set_script(m["whitelist"][0],"whitelist"); update = true; } if(m["blacklist"] !== undefined){ set_script(m["blacklist"][0],"blacklist"); update = true; } if(m["forget"] !== undefined){ set_script(m["forget"][0],"forget"); update = true; } // if(m["open_popup_tab"] !== undefined){ open_popup_tab(m["open_popup_tab"]); } // a debug feature if(m["printlocalstorage"] !== undefined){ debug_print_local(); } // invoke_contact_finder if(m["invoke_contact_finder"] !== undefined){ contact_finder = true; inject_contact_finder(); } // a debug feature (maybe give the user an option to do this?) if(m["deletelocalstorage"] !== undefined){ debug_delete_local(); } function logTabs(tabs) { if(contact_finder){ console.log("[TABID:"+tab_id+"] Injecting contact finder"); //inject_contact_finder(tabs[0]["id"]); } if(update){ console.log("%c updating tab "+tabs[0]["id"],"color: red;"); update_popup(tabs[0]["id"],unused_data[tabs[0]["id"]],true); active_connections[tabs[0]["id"]] = p; } for(var i = 0; i < tabs.length; i++) { var tab = tabs[i]; var tab_id = tab["id"]; if(unused_data[tab_id] !== undefined){ // If we have some data stored here for this tabID, send it console.log("[TABID:"+tab_id+"]"+"Sending stored data associated with browser action"); p.postMessage({"show_info":unused_data[tab_id]}); } else{ // create a new entry unused_data[tab_id] = {"url":tab["url"],"blocked":"","accepted":""}; p.postMessage({"show_info":unused_data[tab_id]}); console.log("[TABID:"+tab_id+"]"+"No data found, creating a new entry for this window."); } } } var querying = webex.tabs.query({active: true,currentWindow: true},logTabs); }); } /** * The callback for tab closings. * * Delete the info we are storing about this tab if there is any. * */ function delete_removed_tab_info(tab_id, remove_info){ console.log("[TABID:"+tab_id+"]"+"Deleting stored info about closed tab"); if(unused_data[tab_id] !== undefined){ delete unused_data[tab_id]; } if(active_connections[tab_id] !== undefined){ delete active_connections[tab_id]; } } /** * Makes it so we can return redirect requests to local blob URLs * * TODO: Make it so that it adds the website itself to the permissions of all keys * */ function change_csp(e) { var index = 0; var csp_header = ""; for(var i = 0; i < e["responseHeaders"].length; i++){ if(e["responseHeaders"][i]["name"].toLowerCase() == "content-security-policy"){ csp_header = e["responseHeaders"][i]["value"]; index = i; var keywords = csp_header.replace(/;/g,'","'); keywords = JSON.parse('["' + keywords.substr(0,keywords.length) + '"]'); // Iterates over the keywords inside the CSP header for(var j = 0; j < keywords.length; j++){ var matchres = keywords[j].match(/[\-\w]+/g); if(matchres != null && matchres[0] == "script-src"){ // Test to see if they have a hash and then delete it // TODO: Make sure this is a good idea. keywords[j] = keywords[j].replace(/\s?'sha256-[\w+/]+=+'/g,""); keywords[j] = keywords[j].replace(/\s?'sha384-[\w+/]+=+'/g,""); keywords[j] = keywords[j].replace(/\s?'sha512-[\w+/]+=+'/g,""); keywords[j] = keywords[j].replace(/'strict-dynamic'/g,""); keywords[j] = keywords[j].replace(/;/g,""); // This is the string that we add to every CSP keywords[j] += " data: blob: 'report-sample'"; //console.log("%c new script-src section:","color:green;") //console.log(keywords[j]+ "; "); } } var csp_header = ""; for(var j = 0; j < keywords.length; j++){ csp_header = csp_header + keywords[j] + "; "; } e["responseHeaders"][i]["value"] = csp_header; } } if(csp_header == ""){ //console.log("%c no CSP.","color: red;"); }else{ //console.log("%c new CSP:","color: green;"); //console.log(e["responseHeaders"][index]["value"]); } return {responseHeaders: e.responseHeaders}; } /* * * XMLHttpRequests the content of a script so we can modify it * before turning it to a blob and redirecting to its URL * */ function get_content(url){ return new Promise((resolve, reject) => { var xhr = new XMLHttpRequest(); xhr.open("get",url); xhr.onload = function(){ resolve(this); } xhr.onerror = function(){ console.log("%c could not get content of "+url+".","color:red;") reject(JSON.stringify(this)); } xhr.send(); }); } /** * Turns a blob URL into a data URL * */ function get_data_url(blob,url){ return new Promise((resolve, reject) => { //var url = URL.createObjectURL(blob); var reader = new FileReader(); reader.addEventListener("load", function(){ //console.log("redirecting"); //console.log(url); //console.log("to"); //console.log(reader.result); resolve({"redirectUrl": reader.result}); }); reader.readAsDataURL(blob); }); } /* *********************************************************************************************** */ // (This is part of eval_test.js with a few console.logs/comments removed) function evaluate(script,name){ function reserved_object_regex(object){ var arith_operators = "\\+\\-\\*\\/\\%\\="; var scope_chars = "\{\}\]\[\(\)\,"; var trailing_chars = "\s*"+"\(\.\["; return new RegExp("(?:[^\\w\\d]|^|(?:"+arith_operators+"))"+object+'(?:\\s*?(?:[\\;\\,\\.\\(\\[])\\s*?)',"g"); } reserved_object_regex("window"); var all_strings = new RegExp('".*?"'+"|'.*?'","gm"); var ml_comment = /\/\*([\s\S]+?)\*\//g; var il_comment = /\/\/.+/gm; var bracket_pairs = /\[.+?\]/g; var temp = script.replace(/'.+?'+/gm,"'string'"); temp = temp.replace(/".+?"+/gm,'"string"'); temp = temp.replace(ml_comment,""); temp = temp.replace(il_comment,""); console.log("------evaluation results for "+ name +"------"); console.log("Script accesses reserved objects?"); var flag = true; var reason = "" // This is where individual "passes" are made over the code for(var i = 0; i < reserved_objects.length; i++){ var res = reserved_object_regex(reserved_objects[i]).exec(script); if(res != null){ console.log("%c fail","color:red;"); flag = false; reason = "Script uses a reserved object (" + reserved_objects[i] + ")"; } } if(flag){ console.log("%c pass","color:green;"); } // If flag is set true at this point, the script is trivial if(flag){ reason = "Script was determined to be trivial."; } return [flag,reason+"
"]; } function license_valid(matches){ if(matches.length != 4){ return [false, "malformed or unrecognized license tag"]; } if(matches[1] != "@license"){ return [false, "malformed or unrecognized license tag"]; } if(licenses[matches[3]] === undefined){ return [false, "malformed or unrecognized license tag"]; } if(licenses[matches[3]]["Magnet link"] != matches[2]){ return [false, "malformed or unrecognized license tag"]; } return [true,"Recognized license as '"+matches[3]+"'
"]; } /** * * Evaluates the content of a script (license, if it is non-trivial) * * Returns * [ * true (accepted) or false (denied), * edited content, * reason text * ] */ function license_read(script_src,name){ var reason_text = ""; var edited_src = ""; var unedited_src = script_src; var nontrivial_status; var parts_denied = false; var parts_accepted = false; while(true){ // TODO: support multiline comments var matches = /\/\/\s*?(@license)\s([\S]+)\s([\S]+$)/gm.exec(unedited_src); if(matches == null){ nontrivial_status = evaluate(unedited_src,name); if(nontrivial_status[0] == true){ parts_accepted = true; edited_src += unedited_src; } else{ parts_denied = true; edited_src += "\n/*\nLIBREJS BLOCKED:"+nontrivial_status[1]+"\n*/\n"; } reason_text += "\n" + nontrivial_status[1]; if(parts_denied == true && parts_accepted == true){ reason_text = "Script was determined partly non-trivial after editing. (check source for details)\n"+reason_text; } if(parts_denied == true && parts_accepted == false){ return [false,edited_src,reason_text]; } return [true,edited_src,reason_text]; } console.log("Found a license tag"); var before = unedited_src.substr(0,matches["index"]); nontrivial_status = evaluate(before,name); if(nontrivial_status[0] == true){ parts_accepted = true; edited_src += before; } else{ parts_denied = true; edited_src += "\n/*\nLIBREJS BLOCKED:"+nontrivial_status[1]+"\n*/\n"; } unedited_src = unedited_src.substr(matches["index"],unedited_src.length); // TODO: support multiline comments var matches_end = /\/\/\s*?(@license-end)/gm.exec(unedited_src); if(matches_end == null){ console.log("ERROR: @license with no @license-end"); return [false,"\n/*\n ERROR: @license with no @license-end \n*/\n","ERROR: @license with no @license-end"]; } var endtag_end_index = matches_end["index"]+matches_end[0].length; var license_res = license_valid(matches); if(license_res[0] == true){ edited_src = edited_src + unedited_src.substr(0,endtag_end_index); reason_text += "\n" + license_res[1]; } else{ edited_src = edited_src + "\n/*\n"+license_res[1]+"\n*/\n"; reason_text += "\n" + license_res[1]; } // trim off everything we just evaluated unedited_src = unedited_src.substr(endtag_end_index,unedited_src.length); } } /* *********************************************************************************************** */ // TODO: Test if this script is being loaded from another domain compared to unused_data[tabid]["url"] // TODO: test if this script is whitelisted by name (from the GUI with the button) function get_script(url,tabid,wl){ return new Promise((resolve, reject) => { var response = get_content(url); response.then(function(response) { if(unused_data[tabid] === undefined){ unused_data[tabid] = {"url":url,"accepted":[],"blocked":[]}; } var tok_index = url.split("/").length; var scriptname = url.split("/")[tok_index-1]; if(wl == true){ // Accept without reading script, it was explicitly whitelisted if(typeof(unused_data[tabid]["accepted"].push) != "function"){ unused_data[tabid]["accepted"] = [[scriptname,"Page is whitelisted in preferences"]]; } else{ unused_data[tabid]["accepted"].push([scriptname,"Page is whitelisted in preferences"]); } var blob = new Blob([response.responseText], {type : 'application/javascript'}); resolve(get_data_url(blob,url)); return; } var src_hash = hash(response.responseText); var edited = license_read(response.responseText,scriptname); var verdict = edited[0]; var popup_res; var domain = get_domain(url); if(verdict == true){ popup_res = add_popup_entry(tabid,src_hash,{"url":domain,"accepted":[scriptname+" ("+src_hash+")",edited[2]]}); } else{ popup_res = add_popup_entry(tabid,src_hash,{"url":domain,"blocked":[scriptname+" ("+src_hash+")",edited[2]]}); } popup_res.then(function(list_verdict){ var blob; if(list_verdict == "wl"){ // redirect to the unedited version blob = new Blob(["\n/*\n LibreJS: Script whitelisted by user \n*/\n"+response.responseText], {type : 'application/javascript'}); }else if(list_verdict == "bl"){ // Blank the entire script blob = new Blob(["\n/*\n LibreJS: Script blacklisted by user \n*/\n"], {type : 'application/javascript'}); } else{ // Return the edited (normal) version blob = new Blob([edited[1]], {type : 'application/javascript'}); } //blob = new Blob(["console.log('LibreJS edited script');\n"+edited[1]], {type : 'application/javascript'}); resolve(get_data_url(blob,url)); }); }); }); } function read_script(a){ return new Promise((resolve, reject) => { var res = test_url_whitelisted(a.url); res.then(function(whitelisted){ if(whitelisted == true){ // Doesn't matter if this is accepted or blocked, it will still be whitelisted resolve(get_script(a.url,a["tabId"],true)); } else{ resolve(get_script(a.url,a["tabId"],false)); } }); }); /* // Minimal example of how to edit scripts var edited = "console.log('it worked');\n"; var blob = new Blob([edited], {type : 'application/javascript'}); return get_data_url(blob); */ } function read_document(a){ // This needs to be handled in a different way because it sets the domain // of the document to "data:" which breaks relative URLs. return new Promise((resolve, reject) => { var response = get_content(a.url); response.then(function(res){ // Reset the block scripts since we just opened a new document unused_data[a["tabId"]] = {"url":a.url,"accepted":[],"blocked":[]}; //setup_counter(res.response,a["tabId"]) resolve(); //var blob = new Blob([res.response], {type : 'text/html'}); //resolve(get_data_url(blob)); }); }); } /** * Initializes various add-on functions * only meant to be called once when the script starts */ function init_addon(){ set_webex(); webex.runtime.onConnect.addListener(connected); webex.storage.onChanged.addListener(options_listener); webex.tabs.onRemoved.addListener(delete_removed_tab_info); var targetPage = "https://developer.mozilla.org/en-US/Firefox/Developer_Edition"; // Updates the content security policy so we can redirect to local URLs webex.webRequest.onHeadersReceived.addListener( change_csp, {urls: [""]}, ["blocking", "responseHeaders"] ); // Analyzes remote scripts webex.webRequest.onBeforeRequest.addListener( read_script, {urls:[""], types:["script"]}, ["blocking"] ); // Analyzes the scripts inside of HTML webex.webRequest.onBeforeRequest.addListener( read_document, {urls:[""], types:["main_frame"]}, ["blocking"] ); } /** * Test if a page is whitelisted/blacklisted. * * The input here is tested against the comma seperated string found in the options. * * It does NOT test against the individual entries created by hitting the "whitelist" * button for a script in the browser action. */ function test_url_whitelisted(url){ return new Promise((resolve, reject) => { function cb(items){ var wl = items["pref_whitelist"]; if(wl !== undefined){ wl = wl.split(","); } else{ resolve(false); return; } var regex; for(var i in wl){ var s = wl[i].replace(/\*/g,"\\S*"); s = s.replace(/\./g,"\\."); regex = new RegExp(s, "g"); if(url.match(regex)){ //console.log("%c" + wl[i] + " matched " + url,"color: purple;"); resolve(true); return; } else{ //console.log("%c" + wl[i] + " didn't match " + url,"color: #dd0000;"); } } resolve(false); return; } webex.storage.local.get(cb); }); } /** * Loads the contact finder on the given tab ID. */ function inject_contact_finder(tab_id){ function executed(result) { console.log("[TABID:"+tab_id+"]"+"finished executing contact finder: " + result); } var executing = webex.tabs.executeScript(tab_id, {file: "/contact_finder.js"}, executed); } init_addon();