zerobin.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. /**
  2. * ZeroBin
  3. *
  4. * a zero-knowledge paste bin
  5. *
  6. * @link http://sebsauvage.net/wiki/doku.php?id=php:zerobin
  7. * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
  8. * @license http://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
  9. * @version 0.19
  10. */
  11. // Immediately start random number generator collector.
  12. sjcl.random.startCollectors();
  13. /**
  14. * Converts a duration (in seconds) into human readable format.
  15. *
  16. * @param int seconds
  17. * @return string
  18. */
  19. function secondsToHuman(seconds)
  20. {
  21. if (seconds<60) { var v=Math.floor(seconds); return v+' second'+((v>1)?'s':''); }
  22. if (seconds<60*60) { var v=Math.floor(seconds/60); return v+' minute'+((v>1)?'s':''); }
  23. if (seconds<60*60*24) { var v=Math.floor(seconds/(60*60)); return v+' hour'+((v>1)?'s':''); }
  24. // If less than 2 months, display in days:
  25. if (seconds<60*60*24*60) { var v=Math.floor(seconds/(60*60*24)); return v+' day'+((v>1)?'s':''); }
  26. var v=Math.floor(seconds/(60*60*24*30)); return v+' month'+((v>1)?'s':'');
  27. }
  28. /**
  29. * Converts an associative array to an encoded string
  30. * for appending to the anchor.
  31. *
  32. * @param object associative_array Object to be serialized
  33. * @return string
  34. */
  35. function hashToParameterString(associativeArray)
  36. {
  37. var parameterString = ""
  38. for (key in associativeArray)
  39. {
  40. if( parameterString === "" )
  41. {
  42. parameterString = encodeURIComponent(key);
  43. parameterString += "=" + encodeURIComponent(associativeArray[key]);
  44. } else {
  45. parameterString += "&" + encodeURIComponent(key);
  46. parameterString += "=" + encodeURIComponent(associativeArray[key]);
  47. }
  48. }
  49. //padding for URL shorteners
  50. parameterString += "&p=p";
  51. return parameterString;
  52. }
  53. /**
  54. * Converts a string to an associative array.
  55. *
  56. * @param string parameter_string String containing parameters
  57. * @return object
  58. */
  59. function parameterStringToHash(parameterString)
  60. {
  61. var parameterHash = {};
  62. var parameterArray = parameterString.split("&");
  63. for (var i = 0; i < parameterArray.length; i++) {
  64. //var currentParamterString = decodeURIComponent(parameterArray[i]);
  65. var pair = parameterArray[i].split("=");
  66. var key = decodeURIComponent(pair[0]);
  67. var value = decodeURIComponent(pair[1]);
  68. parameterHash[key] = value;
  69. }
  70. return parameterHash;
  71. }
  72. /**
  73. * Get an associative array of the parameters found in the anchor
  74. *
  75. * @return object
  76. **/
  77. function getParameterHash()
  78. {
  79. var hashIndex = window.location.href.indexOf("#");
  80. if (hashIndex >= 0) {
  81. return parameterStringToHash(window.location.href.substring(hashIndex + 1));
  82. } else {
  83. return {};
  84. }
  85. }
  86. /**
  87. * Compress a message (deflate compression). Returns base64 encoded data.
  88. *
  89. * @param string message
  90. * @return base64 string data
  91. */
  92. function compress(message) {
  93. return Base64.toBase64( RawDeflate.deflate( Base64.utob(message) ) );
  94. }
  95. /**
  96. * Decompress a message compressed with compress().
  97. */
  98. function decompress(data) {
  99. return Base64.btou( RawDeflate.inflate( Base64.fromBase64(data) ) );
  100. }
  101. /**
  102. * Compress, then encrypt message with key.
  103. *
  104. * @param string key
  105. * @param string message
  106. * @return encrypted string data
  107. */
  108. function zeroCipher(key, message) {
  109. if ($('input#password').val().length == 0) {
  110. return sjcl.encrypt(key, compress(message));
  111. }
  112. return sjcl.encrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash($("input#password").val())), compress(message));
  113. }
  114. /**
  115. * Decrypt message with key, then decompress.
  116. *
  117. * @param key
  118. * @param encrypted string data
  119. * @return string readable message
  120. */
  121. function zeroDecipher(key, data) {
  122. if (data != undefined) {
  123. try {
  124. return decompress(sjcl.decrypt(key, data));
  125. } catch (err) {
  126. var password = prompt("Please enter the password for this paste.", "");
  127. return decompress(sjcl.decrypt(key + sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(password)), data));
  128. }
  129. }
  130. }
  131. /**
  132. * @return the current script location (without search or hash part of the URL).
  133. * eg. http://server.com/zero/?aaaa#bbbb --> http://server.com/zero/
  134. */
  135. function scriptLocation() {
  136. var scriptLocation = window.location.href.substring(0,window.location.href.length
  137. - window.location.search.length - window.location.hash.length);
  138. var hashIndex = scriptLocation.indexOf("#");
  139. if (hashIndex !== -1) {
  140. scriptLocation = scriptLocation.substring(0, hashIndex)
  141. }
  142. return scriptLocation
  143. }
  144. /**
  145. * @return the paste unique identifier from the URL
  146. * eg. 'c05354954c49a487'
  147. */
  148. function pasteID() {
  149. return window.location.search.substring(1);
  150. }
  151. function htmlEntities(str) {
  152. return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  153. }
  154. /**
  155. * Set text of a DOM element (required for IE)
  156. * This is equivalent to element.text(text)
  157. * @param object element : a DOM element.
  158. * @param string text : the text to enter.
  159. */
  160. function setElementText(element, text) {
  161. // For IE<10.
  162. if ($('#oldienotice').is(":visible")) {
  163. // IE<10 does not support white-space:pre-wrap; so we have to do this BIG UGLY STINKING THING.
  164. var html = htmlEntities(text).replace(/\n/ig,"\r\n<br>");
  165. element.html('<pre>'+html+'</pre>');
  166. }
  167. // for other (sane) browsers:
  168. else {
  169. element.text(text);
  170. }
  171. }
  172. /**
  173. * Show decrypted text in the display area, including discussion (if open)
  174. *
  175. * @param string key : decryption key
  176. * @param array comments : Array of messages to display (items = array with keys ('data','meta')
  177. */
  178. function displayMessages(key, comments) {
  179. try { // Try to decrypt the paste.
  180. var cleartext = zeroDecipher(key, comments[0].data);
  181. } catch(err) {
  182. $('#cleartext').addClass('hidden');
  183. $('#prettymessage').addClass('hidden');
  184. $('#clonebutton').addClass('hidden');
  185. showError('Could not decrypt data (Wrong key ?)');
  186. return;
  187. }
  188. setElementText($('#cleartext'), cleartext);
  189. setElementText($('#prettyprint'), cleartext);
  190. // Convert URLs to clickable links.
  191. urls2links($('#cleartext'));
  192. urls2links($('#prettyprint'));
  193. if (typeof prettyPrint == 'function') prettyPrint();
  194. // Display paste expiration.
  195. if (comments[0].meta.expire_date) $('#remainingtime').removeClass('foryoureyesonly').text('This document will expire in '+secondsToHuman(comments[0].meta.remaining_time)+'.').removeClass('hidden');
  196. if (comments[0].meta.burnafterreading) {
  197. $('#remainingtime').addClass('foryoureyesonly').text('FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.').removeClass('hidden');
  198. $('#clonebutton').addClass('hidden'); // Discourage cloning (as it can't really be prevented).
  199. }
  200. // If the discussion is opened on this paste, display it.
  201. if (comments[0].meta.opendiscussion) {
  202. $('#comments').html('');
  203. // For each comment.
  204. for (var i = 1; i < comments.length; i++) {
  205. var comment=comments[i];
  206. var cleartext="[Could not decrypt comment ; Wrong key ?]";
  207. try {
  208. cleartext = zeroDecipher(key, comment.data);
  209. } catch(err) { }
  210. var place = $('#comments');
  211. // If parent comment exists, display below (CSS will automatically shift it right.)
  212. var cname = '#comment_'+comment.meta.parentid
  213. // If the element exists in page
  214. if ($(cname).length) {
  215. place = $(cname);
  216. }
  217. var divComment = $('<article><div class="comment" id="comment_' + comment.meta.commentid+'">'
  218. + '<div class="commentmeta"><span class="nickname"></span><span class="commentdate"></span></div><div class="commentdata"></div>'
  219. + '<button onclick="open_reply($(this),\'' + comment.meta.commentid + '\');return false;" class="btn btn-default">Reply</button>'
  220. + '</div></article>');
  221. setElementText(divComment.find('div.commentdata'), cleartext);
  222. // Convert URLs to clickable links in comment.
  223. urls2links(divComment.find('div.commentdata'));
  224. divComment.find('span.nickname').html('<i>(Anonymous)</i>');
  225. // Try to get optional nickname:
  226. try {
  227. divComment.find('span.nickname').text(zeroDecipher(key, comment.meta.nickname));
  228. } catch(err) { }
  229. divComment.find('span.commentdate').text(' ('+(new Date(comment.meta.postdate*1000).toString())+')').attr('title','CommentID: ' + comment.meta.commentid);
  230. // If an avatar is available, display it.
  231. if (comment.meta.vizhash) {
  232. divComment.find('span.nickname').before('<img src="' + comment.meta.vizhash + '" class="vizhash" title="Anonymous avatar (Vizhash of the IP address)" />');
  233. }
  234. place.append(divComment);
  235. }
  236. $('#comments').append('<div class="comment"><button onclick="open_reply($(this),\'' + pasteID() + '\');return false;" class="btn btn-default">Add comment</button></div>');
  237. $('#discussion').removeClass('hidden');
  238. }
  239. }
  240. /**
  241. * Open the comment entry when clicking the "Reply" button of a comment.
  242. * @param object source : element which emitted the event.
  243. * @param string commentid = identifier of the comment we want to reply to.
  244. */
  245. function open_reply(source, commentid) {
  246. $('div.reply').remove(); // Remove any other reply area.
  247. source.after('<div class="reply">'
  248. + '<input type="text" id="nickname" class="form-control" title="Optional nickname..." value="Optional nickname..." />'
  249. + '<textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea>'
  250. + '<br /><button id="replybutton" onclick="send_comment(\'' + commentid + '\');return false;" class="btn btn-default">Post comment</button>'
  251. + '<div id="replystatus"> </div>'
  252. + '</div>');
  253. $('#nickname').focus(function() {
  254. if ($(this).val() == $(this).attr('title')) {
  255. $(this).val('');
  256. }
  257. });
  258. $('#replymessage').focus();
  259. }
  260. /**
  261. * Send a reply in a discussion.
  262. * @param string parentid : the comment identifier we want to send a reply to.
  263. */
  264. function send_comment(parentid) {
  265. // Do not send if no data.
  266. if ($('#replymessage').val().length==0) {
  267. return;
  268. }
  269. showStatus('Sending comment...', spin=true);
  270. var cipherdata = zeroCipher(pageKey(), $('#replymessage').val());
  271. var ciphernickname = '';
  272. var nick=$('#nickname').val();
  273. if (nick != '' && nick != 'Optional nickname...') {
  274. ciphernickname = zeroCipher(pageKey(), nick);
  275. }
  276. var data_to_send = { data:cipherdata,
  277. parentid: parentid,
  278. pasteid: pasteID(),
  279. nickname: ciphernickname
  280. };
  281. $.post(scriptLocation(), data_to_send, 'json')
  282. .error(function() {
  283. showError('Comment could not be sent (server error or not responding).');
  284. })
  285. .success(function(data) {
  286. if (data.status == 0) {
  287. showStatus('Comment posted.');
  288. location.reload();
  289. }
  290. else if (data.status==1) {
  291. showError('Could not post comment: '+data.message);
  292. }
  293. else {
  294. showError('Could not post comment.');
  295. }
  296. });
  297. }
  298. /**
  299. * Send a new paste to server
  300. */
  301. function send_data() {
  302. // Do not send if no data.
  303. if ($('#message').val().length == 0) {
  304. return;
  305. }
  306. // If sjcl has not collected enough entropy yet, display a message.
  307. if (!sjcl.random.isReady())
  308. {
  309. showStatus('Sending paste (Please move your mouse for more entropy)...', spin=true);
  310. sjcl.random.addEventListener('seeded', function(){ send_data(); });
  311. return;
  312. }
  313. showStatus('Sending paste...', spin=true);
  314. var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0);
  315. var cipherdata = zeroCipher(randomkey, $('#message').val());
  316. var data_to_send = { data: cipherdata,
  317. expire: $('#pasteExpiration').val(),
  318. burnafterreading: $('#burnafterreading').is(':checked') ? 1 : 0,
  319. opendiscussion: $('#opendiscussion').is(':checked') ? 1 : 0
  320. };
  321. $.post(scriptLocation(), data_to_send, 'json')
  322. .error(function() {
  323. showError('Data could not be sent (serveur error or not responding).');
  324. })
  325. .success(function(data) {
  326. if (data.status == 0) {
  327. stateExistingPaste();
  328. var url = scriptLocation() + "?" + data.id + '#' + randomkey;
  329. var deleteUrl = scriptLocation() + "?pasteid=" + data.id + '&deletetoken=' + data.deletetoken;
  330. showStatus('');
  331. $('#pastelink').html('Your paste is <a id="pasteurl" href="' + url + '">' + url + '</a> <span id="copyhint">(Hit CTRL+C to copy)</span>');
  332. $('#deletelink').html('<a href="' + deleteUrl + '">Delete data</a>');
  333. $('#pasteresult').removeClass('hidden');
  334. selectText('pasteurl'); // We pre-select the link so that the user only has to CTRL+C the link.
  335. setElementText($('#cleartext'), $('#message').val());
  336. setElementText($('#prettyprint'), $('#message').val());
  337. // Convert URLs to clickable links.
  338. urls2links($('#cleartext'));
  339. urls2links($('#prettyprint'));
  340. showStatus('');
  341. if (typeof prettyPrint == 'function') prettyPrint();
  342. }
  343. else if (data.status==1) {
  344. showError('Could not create paste: '+data.message);
  345. }
  346. else {
  347. showError('Could not create paste.');
  348. }
  349. });
  350. }
  351. /** Text range selection.
  352. * From: http://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse
  353. * @param string element : Indentifier of the element to select (id="").
  354. */
  355. function selectText(element) {
  356. var doc = document
  357. , text = doc.getElementById(element)
  358. , range, selection
  359. ;
  360. if (doc.body.createTextRange) { //ms
  361. range = doc.body.createTextRange();
  362. range.moveToElementText(text);
  363. range.select();
  364. } else if (window.getSelection) { //all others
  365. selection = window.getSelection();
  366. range = doc.createRange();
  367. range.selectNodeContents(text);
  368. selection.removeAllRanges();
  369. selection.addRange(range);
  370. }
  371. }
  372. /**
  373. * Put the screen in "New paste" mode.
  374. */
  375. function stateNewPaste() {
  376. $('#sendbutton').removeClass('hidden');
  377. $('#clonebutton').addClass('hidden');
  378. $('#rawtextbutton').addClass('hidden');
  379. $('#expiration').removeClass('hidden');
  380. $('#remainingtime').addClass('hidden');
  381. $('#burnafterreadingoption').removeClass('hidden');
  382. $('#opendisc').removeClass('hidden');
  383. $('#newbutton').removeClass('hidden');
  384. $('#pasteresult').addClass('hidden');
  385. $('#message').text('');
  386. $('#message').removeClass('hidden');
  387. $('#cleartext').addClass('hidden');
  388. $('#message').focus();
  389. $('#discussion').addClass('hidden');
  390. $('#prettymessage').addClass('hidden');
  391. // Show password field
  392. $('#password').show();
  393. }
  394. /**
  395. * Put the screen in "Existing paste" mode.
  396. */
  397. function stateExistingPaste() {
  398. $('#sendbutton').addClass('hidden');
  399. // No "clone" for IE<10.
  400. if ($('#oldienotice').is(":visible")) {
  401. $('#clonebutton').addClass('hidden');
  402. }
  403. else {
  404. $('#clonebutton').removeClass('hidden');
  405. }
  406. $('#rawtextbutton').removeClass('hidden');
  407. $('#expiration').addClass('hidden');
  408. $('#burnafterreadingoption').addClass('hidden');
  409. $('#opendisc').addClass('hidden');
  410. $('#newbutton').removeClass('hidden');
  411. $('#pasteresult').addClass('hidden');
  412. $('#message').addClass('hidden');
  413. $('#cleartext').addClass('hidden');
  414. $('#prettymessage').removeClass('hidden');
  415. }
  416. /** Return raw text
  417. */
  418. function rawText()
  419. {
  420. var paste = $('#cleartext').html();
  421. var newDoc = document.open('text/html', 'replace');
  422. newDoc.write('<pre>'+paste+'</pre>');
  423. newDoc.close();
  424. }
  425. /**
  426. * Clone the current paste.
  427. */
  428. function clonePaste() {
  429. stateNewPaste();
  430. //Erase the id and the key in url
  431. history.replaceState(document.title, document.title, scriptLocation());
  432. showStatus('');
  433. $('#message').text($('#cleartext').text());
  434. }
  435. /**
  436. * Create a new paste.
  437. */
  438. function newPaste() {
  439. stateNewPaste();
  440. showStatus('');
  441. $('#message').text('');
  442. }
  443. /**
  444. * Display an error message
  445. * (We use the same function for paste and reply to comments)
  446. */
  447. function showError(message) {
  448. $('#status').addClass('errorMessage').text(message);
  449. $('#replystatus').addClass('errorMessage').text(message);
  450. }
  451. /**
  452. * Display status
  453. * (We use the same function for paste and reply to comments)
  454. *
  455. * @param string message : text to display
  456. * @param boolean spin (optional) : tell if the "spinning" animation should be displayed.
  457. */
  458. function showStatus(message, spin) {
  459. $('#replystatus').removeClass('errorMessage');
  460. $('#replystatus').text(message);
  461. if (!message) {
  462. $('#status').html(' ');
  463. return;
  464. }
  465. if (message == '') {
  466. $('#status').html(' ');
  467. return;
  468. }
  469. $('#status').removeClass('errorMessage');
  470. $('#status').text(message);
  471. if (spin) {
  472. var img = '<img src="img/busy.gif" style="width:16px;height:9px;margin:0px 4px 0px 0px;" />';
  473. $('#status').prepend(img);
  474. $('#replystatus').prepend(img);
  475. }
  476. }
  477. /**
  478. * Convert URLs to clickable links.
  479. * URLs to handle:
  480. * <code>
  481. * magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
  482. * http://localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
  483. * http://user:password@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
  484. * </code>
  485. *
  486. * @param object element : a jQuery DOM element.
  487. * @FIXME: add ppa & apt links.
  488. */
  489. function urls2links(element) {
  490. var re = /((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig;
  491. element.html(element.html().replace(re,'<a href="$1" rel="nofollow">$1</a>'));
  492. var re = /((magnet):[\w?=&.\/-;#@~%+-]+)/ig;
  493. element.html(element.html().replace(re,'<a href="$1">$1</a>'));
  494. }
  495. /**
  496. * Return the deciphering key stored in anchor part of the URL
  497. */
  498. function pageKey() {
  499. var key = window.location.hash.substring(1); // Get key
  500. // Some stupid web 2.0 services and redirectors add data AFTER the anchor
  501. // (such as &utm_source=...).
  502. // We will strip any additional data.
  503. // First, strip everything after the equal sign (=) which signals end of base64 string.
  504. i = key.indexOf('='); if (i>-1) { key = key.substring(0,i+1); }
  505. // If the equal sign was not present, some parameters may remain:
  506. i = key.indexOf('&'); if (i>-1) { key = key.substring(0,i); }
  507. // Then add trailing equal sign if it's missing
  508. if (key.charAt(key.length-1)!=='=') key+='=';
  509. return key;
  510. }
  511. $(function() {
  512. // hide "no javascript" message
  513. $('#noscript').hide();
  514. // If "burn after reading" is checked, disable discussion.
  515. $('#burnafterreading').change(function() {
  516. if ($(this).is(':checked') ) {
  517. $('#opendisc').addClass('buttondisabled');
  518. $('#opendiscussion').attr({checked: false});
  519. $('#opendiscussion').attr('disabled',true);
  520. }
  521. else {
  522. $('#opendisc').removeClass('buttondisabled');
  523. $('#opendiscussion').removeAttr('disabled');
  524. }
  525. });
  526. // Display status returned by php code if any (eg. Paste was properly deleted.)
  527. if ($('#status').text().length > 0) {
  528. showStatus($('#status').text(),false);
  529. return;
  530. }
  531. $('#status').html(' '); // Keep line height even if content empty.
  532. // Display an existing paste
  533. if ($('#cipherdata').text().length > 1) {
  534. // Missing decryption key in URL ?
  535. if (window.location.hash.length == 0) {
  536. showError('Cannot decrypt paste: Decryption key missing in URL (Did you use a redirector or an URL shortener which strips part of the URL ?)');
  537. return;
  538. }
  539. // List of messages to display
  540. var messages = jQuery.parseJSON($('#cipherdata').text());
  541. // Show proper elements on screen.
  542. stateExistingPaste();
  543. displayMessages(pageKey(), messages);
  544. }
  545. // Display error message from php code.
  546. else if ($('#errormessage').text().length>1) {
  547. showError($('#errormessage').text());
  548. }
  549. // Create a new paste.
  550. else {
  551. newPaste();
  552. }
  553. });