zerobin.js 21 KB

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