├── .babelrc ├── .gitignore ├── static ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── JetBrainsMono-Regular.ttf ├── JetBrainsMono-Regular.woff ├── JetBrainsMono-Regular.woff2 ├── KFOmCnqEu92Fr1Mu4mxK.woff2 ├── android-chrome-192x192.png └── android-chrome-512x512.png ├── src ├── index.js ├── encryption.js ├── interface.js ├── account.js ├── links.js ├── utility.js ├── core.css └── editor.js ├── manifest.json ├── .github └── workflows │ └── deploy-to-skynet.yml ├── LICENSE ├── webpack.config.js ├── package.json ├── README.md └── index.html /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": "es2015" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harej/hackerpaste/HEAD/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harej/hackerpaste/HEAD/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harej/hackerpaste/HEAD/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/JetBrainsMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harej/hackerpaste/HEAD/static/JetBrainsMono-Regular.ttf -------------------------------------------------------------------------------- /static/JetBrainsMono-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harej/hackerpaste/HEAD/static/JetBrainsMono-Regular.woff -------------------------------------------------------------------------------- /static/JetBrainsMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harej/hackerpaste/HEAD/static/JetBrainsMono-Regular.woff2 -------------------------------------------------------------------------------- /static/KFOmCnqEu92Fr1Mu4mxK.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harej/hackerpaste/HEAD/static/KFOmCnqEu92Fr1Mu4mxK.woff2 -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harej/hackerpaste/HEAD/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harej/hackerpaste/HEAD/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 8 */ 2 | 3 | import { initCodeEditor, initLangSelector, initCode, initListeners, 4 | initKeyboardShortcuts } 5 | from './editor.js'; 6 | import { initModals, initClipboard } 7 | from './interface.js'; 8 | 9 | import './core.css'; 10 | 11 | initCodeEditor(); 12 | initLangSelector(); 13 | initCode(); 14 | initClipboard(); 15 | initModals(); 16 | initKeyboardShortcuts(); 17 | initListeners(); 18 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hacker Paste", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "static/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "static/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone", 19 | "skylink": "AQDn5uriOXZFLumP0QhJUO7D3kPkJ2SPciJCiShDnXb1Dw" 20 | } 21 | -------------------------------------------------------------------------------- /src/encryption.js: -------------------------------------------------------------------------------- 1 | /* jshint esversion: 8 */ 2 | 3 | import CryptoJS from 'crypto-js'; 4 | 5 | export const encryptData = (data, docKey) => CryptoJS.AES.encrypt(data, docKey); 6 | 7 | export const decryptData = (data, docKey) => 8 | CryptoJS.enc.Utf8.stringify(CryptoJS.AES.decrypt(data, docKey)); 9 | 10 | export const encryptObject = (data, seed) => { 11 | data = JSON.stringify(data); 12 | let encryptedData = encryptData(data, seed); 13 | data = {encrypted:encryptedData.toString()}; 14 | return data; 15 | }; 16 | 17 | export const decryptObject = (data, seed) => 18 | decryptData(data.encrypted, seed); 19 | -------------------------------------------------------------------------------- /.github/workflows/deploy-to-skynet.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Skynet 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | node-version: 16.x 18 | 19 | - run: npm i 20 | - run: npm run build 21 | 22 | - name: Deploy to Skynet 23 | uses: skynetlabs/deploy-to-skynet-action@v2 24 | with: 25 | upload-dir: build 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | registry-seed: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && secrets.REGISTRY_SEED || '' }} 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 James Hare 4 | 5 | Derivative of NoPaste, (c) 2020 Boris K 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const CopyPlugin = require("copy-webpack-plugin"); 4 | 5 | module.exports = { 6 | plugins: [ 7 | new MiniCssExtractPlugin(), 8 | new CopyPlugin({ 9 | patterns: [ 10 | { from: "static", to: path.resolve(__dirname, 'build/static')}, 11 | { from: "manifest.json", to: path.resolve(__dirname, "build") }, 12 | { from: "index.html", to: path.resolve(__dirname, 'build')}, 13 | ], 14 | }), 15 | ], 16 | module: { 17 | rules:[ 18 | { 19 | test: /\.css$/, 20 | use: [ 21 | { 22 | loader: MiniCssExtractPlugin.loader, 23 | options: { 24 | publicPath: '', 25 | modules: { 26 | namedExport: true 27 | } 28 | } 29 | }, 30 | { 31 | loader: 'css-loader' 32 | } 33 | ] 34 | }, 35 | { 36 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 37 | use: [ 38 | 'file-loader' 39 | ] 40 | } 41 | ] 42 | }, 43 | entry: './src/index.js', 44 | output: { 45 | path: path.resolve(__dirname, 'build/dist'), 46 | filename: 'bundle.js' 47 | }, 48 | resolve: { 49 | fallback: { 50 | "crypto": require.resolve("crypto-browserify"), 51 | "stream": require.resolve("stream-browserify"), 52 | "buffer": require.resolve("buffer/") 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/interface.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 8 */ 2 | 3 | import ClipboardJS from 'clipboard'; 4 | 5 | import 'bootstrap/dist/css/bootstrap-grid.min.css'; 6 | import 'microtip/microtip.css'; 7 | 8 | export var clipboard; 9 | 10 | export const byId = (id) => document.getElementById(id); 11 | 12 | export const byClass = (id) => document.getElementsByClassName(id)[0]; 13 | 14 | export const initModals = () => { 15 | MicroModal.init({ 16 | onClose: () => editor.focus(), 17 | }); 18 | }; 19 | 20 | export const initClipboard = () => { 21 | clipboard = new ClipboardJS(".clipboard"); 22 | clipboard.on("success", () => { 23 | hideCopyBar(true); 24 | }); 25 | }; 26 | 27 | export const clickListener = (element_id, func) => 28 | byId(element_id).addEventListener("click", func); 29 | 30 | export const deleteClickListener = (element_id, func) => 31 | byId(element_id).removeEventListener("click", func); 32 | 33 | // Open the "Copy" bar and select the content 34 | export const showCopyBar = (dataToCopy) => { 35 | byId("copy").classList.remove("hidden"); 36 | const linkInput = byId("copy-link"); 37 | linkInput.value = dataToCopy; 38 | linkInput.focus(); 39 | linkInput.setSelectionRange(0, dataToCopy.length); 40 | }; 41 | 42 | // Close the "Copy" bar 43 | export const hideCopyBar = (success) => { 44 | const copyButton = byId("copy-btn"); 45 | const copyBar = byId("copy"); 46 | if (!success) { 47 | copyBar.classList.add("hidden"); 48 | return; 49 | } 50 | copyButton.innerText = "Copied !"; 51 | setTimeout(() => { 52 | copyBar.classList.add("hidden"); 53 | copyButton.innerText = "Copy"; 54 | }, 800); 55 | }; 56 | 57 | export const hideCopyBarNow = () => hideCopyBar(false); 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackerpaste", 3 | "version": "4.0.0", 4 | "description": "The Secure and Decentralized Paste Bin", 5 | "main": "core.js", 6 | "dependencies": { 7 | "babel": "^6.23.0", 8 | "blakejs": "^1.1.0", 9 | "bootstrap": "^4.6.0", 10 | "buffer": "^6.0.3", 11 | "clipboard": "^2.0.6", 12 | "codemirror": "^5.63.0", 13 | "crypto-browserify": "^3.12.0", 14 | "crypto-js": "^4.0.0", 15 | "file-loader": "^6.2.0", 16 | "jquery": "^3.5.1", 17 | "marked": "^3.0.4", 18 | "micromodal": "^0.4.6", 19 | "microtip": "^0.2.2", 20 | "popper.js": "^1.16.1", 21 | "rollup": "^2.35.1", 22 | "slim-select": "^1.25.0", 23 | "stream-browserify": "^3.0.0" 24 | }, 25 | "devDependencies": { 26 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 27 | "@rollup/plugin-node-resolve": "^11.0.1", 28 | "copy-webpack-plugin": "^9.0.1", 29 | "css-loader": "^5.0.2", 30 | "mini-css-extract-plugin": "^1.3.7", 31 | "style-loader": "^2.0.0", 32 | "webpack": "^5.53.0", 33 | "webpack-cli": "^4.2.0" 34 | }, 35 | "scripts": { 36 | "build": "webpack", 37 | "test": "echo \"Error: no test specified\" && exit 1" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/harej/hackerpaste.git" 42 | }, 43 | "keywords": [ 44 | "pastebin", 45 | "paste", 46 | "text", 47 | "snippets", 48 | "text", 49 | "files", 50 | "secure", 51 | "decentralized", 52 | "skynet", 53 | "sia" 54 | ], 55 | "author": "James Hare", 56 | "license": "MIT", 57 | "bugs": { 58 | "url": "https://github.com/harej/hackerpaste/issues" 59 | }, 60 | "homepage": "https://github.com/harej/hackerpaste#readme" 61 | } 62 | -------------------------------------------------------------------------------- /src/account.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 8 */ 2 | 3 | import MicroModal from 'micromodal'; 4 | import { loadMyPastes, myPastes } 5 | from './editor.js'; 6 | import { encryptObject, decryptObject } 7 | from './encryption.js'; 8 | import { byId, clickListener, deleteClickListener } 9 | from './interface.js'; 10 | import { generateDocKey, getPubkeyBasedRetrievalString } 11 | from './utility.js'; 12 | 13 | export var username; 14 | export var pubkey; 15 | 16 | const switchToLoggedIn = (message) => { 17 | if (message == "login_success") { 18 | byId("button-log-out").style.display = "inline-block"; 19 | byId("save-to-my-pastes-button").style.display = "inline-block"; 20 | byId("modal-content").innerHTML = 21 | "

Setting up your Hacker Paste account...

"; 22 | MicroModal.show('app-modal'); 23 | skyid.getRegistry('hackerpaste:username', (response) => { 24 | if (response.entry === null) { 25 | username = prompt("What should we call you?"); 26 | skyid.setRegistry('hackerpaste:username', username, () => 27 | location.reload()); 28 | } else { 29 | username = response.entry.data; 30 | skyid.getProfile((response2) => { 31 | /*response2 = JSON.parse(response2);*/ 32 | pubkey = response2.dapps["Hacker Paste"].publicKey; 33 | byId("username").textContent = username; 34 | byId("button-username").setAttribute('aria-label', 'View My Pastes'); 35 | byId("button-username").setAttribute('data-microtip-size', 'fit'); 36 | skyid.getJSON('hackerpaste:my-pastes', (response3) => { 37 | if (response3 !== null) { 38 | myPastes = decryptObject(response3, skyid.seed); 39 | deleteClickListener("button-username", startSkyIDSession); 40 | clickListener("button-username", loadMyPastes); 41 | MicroModal.close('app-modal'); 42 | } else { 43 | let noteToSelf = getPubkeyBasedRetrievalString( 44 | pubkey) + generateDocKey(); 45 | let defaultContent = 46 | {documents:[{label:"Note to Self",docID:noteToSelf}]}; 47 | defaultContent = encryptObject(defaultContent, skyid.seed); 48 | skyid.setJSON('hackerpaste:my-pastes', 49 | defaultContent, () => location.reload()); 50 | } 51 | }); 52 | }); 53 | } 54 | }); 55 | } 56 | }; 57 | 58 | export var skyid = new SkyID('Hacker Paste', switchToLoggedIn, { 59 | devMode: false 60 | }); 61 | 62 | if (skyid.seed != "") switchToLoggedIn("login_success"); 63 | 64 | export const startSkyIDSession = () => skyid.sessionStart(); 65 | 66 | export const switchToLoggedOut = () => { 67 | skyid.sessionDestroy(); 68 | byId("username").textContent = "Sign in with SkyID"; 69 | clickListener("button-username", startSkyIDSession); 70 | deleteClickListener("button-username", loadMyPastes); 71 | byId("button-log-out").style.display = "none"; 72 | byId("save-to-my-pastes-button").style.display = "none"; 73 | byId("button-username").setAttribute('aria-label', 74 | 'Sign in to keep your own private paste list. Registration is completely anonymous.'); 75 | byId("button-username").setAttribute('data-microtip-size', 'medium'); 76 | }; 77 | -------------------------------------------------------------------------------- /src/links.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 8 */ 2 | 3 | import { skyid, pubkey } 4 | from './account.js'; 5 | import { editor, docLabel, persistentDocKey, myPastes, updateMyPastes, select } 6 | from './editor.js'; 7 | import { encryptData, decryptObject } 8 | from './encryption.js'; 9 | import { showCopyBar } 10 | from './interface.js'; 11 | import { shorten, generateDocKey, generateUuid, getPubkeyBasedRetrievalString } 12 | from './utility.js'; 13 | 14 | const generateLink = (mode) => { 15 | let docKey; 16 | let retrievalString; 17 | if (mode === 'mypastes') { 18 | docKey = persistentDocKey || generateDocKey(); 19 | persistentDocKey = docKey; 20 | } else { 21 | docKey = generateDocKey(); 22 | } 23 | 24 | const data = editor.getValue(); 25 | const encryptedData = encryptData(data, docKey); 26 | showCopyBar('Uploading...'); 27 | var blob = new Blob( 28 | [encryptedData], { 29 | type: "text/plain", 30 | encoding: "utf-8" 31 | } 32 | ); 33 | skyid.skynetClient.uploadFile(blob) 34 | .then((result) => { 35 | if (mode !== 'mypastes') { 36 | retrievalString = result.skylink.replace('sia:', ''); 37 | } 38 | else { 39 | retrievalString = getPubkeyBasedRetrievalString(pubkey); 40 | } 41 | var url = buildUrl(retrievalString, mode, docKey); 42 | if (mode === 'mypastes') { 43 | skyid.getJSON('hackerpaste:my-pastes', (response3) => { 44 | if (response3 !== null) { 45 | myPastes = decryptObject(response3, skyid.seed); 46 | myPastes = JSON.parse(myPastes); 47 | console.log(myPastes); 48 | postFileToRegistry(result.skylink, docKey, url); 49 | var docID = retrievalString + docKey; 50 | var docFound = false; 51 | for (let i = 0; i < myPastes.documents.length; i++) { 52 | if (myPastes.documents[i].docID == docID) { 53 | docLabel = myPastes.documents[i].label; 54 | docFound = true; 55 | } 56 | }; 57 | docLabel = docLabel || prompt("Add a label to this document. Only you can see this label."); 58 | if (!docFound) { 59 | updateMyPastes(docID, docLabel); 60 | } 61 | } 62 | }); 63 | } else { 64 | window.location = url.url; 65 | showCopyBar(url.content); 66 | } 67 | }) 68 | .catch((error) => { 69 | console.error(error); 70 | }); 71 | }; 72 | 73 | export const buildUrl = (retrievalString, mode, docKey) => { 74 | const base = 75 | `${location.protocol}//${location.host}${location.pathname}`; 76 | const lang = shorten("Plain Text") === select.selected() ? "" : 77 | `.${encodeURIComponent(select.selected())}`; 78 | const url = base + "#" + retrievalString + docKey + lang; 79 | if (mode === "iframe") { 80 | const height = editor.doc.height + 45; 81 | let content = 82 | ``; 83 | return { 84 | url: url, 85 | content: content 86 | }; 87 | } 88 | return { 89 | url: url, 90 | content: url 91 | }; 92 | }; 93 | 94 | export const generateEmbed = () => generateLink('iframe'); 95 | 96 | export const generateSnapshotUrl = () => generateLink('url'); 97 | 98 | export const generatePersistentUrl = () => generateLink('mypastes'); 99 | 100 | async function postFileToRegistry(skylink, docKey, url) { 101 | skyid.setRegistry(`hackerpaste:file:${docKey}`, skylink, (success) => { 102 | if (success !== false) { 103 | window.location = url.url; 104 | showCopyBar(url.content); 105 | skyid.hideOverlay(skyid.opts); 106 | } 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /src/utility.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 8 */ 2 | 3 | const slugify = (str) => 4 | str 5 | .trim() 6 | .toString() 7 | .toLowerCase() 8 | .replace(/\s+/g, "-") 9 | .replace(/\+/g, "-p") 10 | .replace(/#/g, "-sharp") 11 | .replace(/[^\w\-]+/g, ""); 12 | 13 | // https://gist.github.com/GeorgioWan/16a7ad2a255e8d5c7ed1aca3ab4aacec 14 | const hexToBase64 = (str) => { 15 | return btoa(String.fromCharCode.apply(null, 16 | str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ") 17 | .replace(/ +$/, "").split(" "))).replace('+', '-').replace('/', '_'); 18 | }; 19 | 20 | // https://gist.github.com/GeorgioWan/16a7ad2a255e8d5c7ed1aca3ab4aacec 21 | export const base64ToHex = (str) => { 22 | for (var i = 0, bin = atob(str.replace(/[ \r\n]+$/, "").replace('-', 23 | '+') 24 | .replace('_', '/')), hex = []; i < bin 25 | .length; ++i) { 26 | let tmp = bin.charCodeAt(i).toString(16); 27 | if (tmp.length === 1) tmp = "0" + tmp; 28 | hex[hex.length] = tmp; 29 | } 30 | return hex.join(""); 31 | }; 32 | 33 | export const shorten = (name) => { 34 | let n = slugify(name).replace("script", "-s").replace("python", "py"); 35 | const nov = (s) => s[0] + s.substr(1).replace(/[aeiouy-]/g, ""); 36 | if (n.replace(/-/g, "").length <= 4) { 37 | return n.replace(/-/g, ""); 38 | } 39 | if (n.split("-").length >= 2) { 40 | return n 41 | .split("-") 42 | .map((x) => nov(x.substr(0, 2))) 43 | .join("") 44 | .substr(0, 4); 45 | } 46 | n = nov(n); 47 | if (n.length <= 4) { 48 | return n; 49 | } 50 | return n.substr(0, 2) + n.substr(n.length - 2, 2); 51 | }; 52 | 53 | export const generateDocKey = () => generateRandomString.url(20); 54 | 55 | export const generateUuid = () => generateRandomString.alphanumeric(16); 56 | 57 | export const getPubkeyBasedRetrievalString = (pubkey) => 58 | hexToBase64(pubkey + '00'); 59 | 60 | // Source: 61 | // https://gist.githubusercontent.com/dchest/751fd00ee417c947c252/raw/53c4e953b4748f4a46367fc1bce4aee8cfc4a1cb/randomString.js 62 | 63 | var generateRandomString = (function() { 64 | 65 | var getRandomBytes = ( 66 | (typeof self !== 'undefined' && (self.crypto || self.msCrypto)) 67 | ? function() { // Browsers 68 | var crypto = (self.crypto || self.msCrypto), QUOTA = 65536; 69 | return function(n) { 70 | var a = new Uint8Array(n); 71 | for (var i = 0; i < n; i += QUOTA) { 72 | crypto.getRandomValues(a.subarray(i, i + Math.min(n - i, QUOTA))); 73 | } 74 | return a; 75 | }; 76 | } 77 | : function() { // Node 78 | return require("crypto").randomBytes; 79 | } 80 | )(); 81 | 82 | var makeGenerator = function(charset) { 83 | if (charset.length < 2) { 84 | throw new Error('charset must have at least 2 characters'); 85 | } 86 | 87 | var generate = function(length) { 88 | if (!length) return generate.entropy(128); 89 | var out = ''; 90 | var charsLen = charset.length; 91 | var maxByte = 256 - (256 % charsLen); 92 | while (length > 0) { 93 | var buf = getRandomBytes(Math.ceil(length * 256 / maxByte)); 94 | for (var i = 0; i < buf.length && length > 0; i++) { 95 | var randomByte = buf[i]; 96 | if (randomByte < maxByte) { 97 | out += charset.charAt(randomByte % charsLen); 98 | length--; 99 | } 100 | } 101 | } 102 | return out; 103 | }; 104 | 105 | generate.entropy = function(bits) { 106 | return generate(Math.ceil(bits / (Math.log(charset.length) / Math.LN2))); 107 | }; 108 | 109 | generate.charset = charset; 110 | 111 | return generate; 112 | }; 113 | 114 | 115 | // Charsets 116 | 117 | var numbers = '0123456789', letters = 'abcdefghijklmnopqrstuvwxyz'; 118 | 119 | var CHARSETS = { 120 | numeric: numbers, 121 | hex: numbers + 'abcdef', 122 | alphalower: letters, 123 | alpha: letters + letters.toUpperCase(), 124 | alphanumeric: numbers + letters + letters.toUpperCase(), 125 | base64: numbers + letters + letters.toUpperCase() + '+/', 126 | url: numbers + letters + letters.toUpperCase() + '-_' 127 | }; 128 | 129 | // Functions 130 | 131 | var randomString = makeGenerator(CHARSETS.alphanumeric); 132 | 133 | for (var name in CHARSETS) { 134 | randomString[name] = makeGenerator(CHARSETS[name]); 135 | } 136 | 137 | randomString.custom = makeGenerator; 138 | 139 | // Tests 140 | 141 | var TESTS = { 142 | length: function(fn) { 143 | if (fn().length !== fn.entropy(128).length) { 144 | throw new Error('Bad result for zero length'); 145 | } 146 | for (var i = 1; i < 32; i++) { 147 | if (fn(i).length !== i) { 148 | throw new Error('Length differs: ' + i); 149 | } 150 | } 151 | }, 152 | chars: function(fn) { 153 | var chars = Array.prototype.map.call(fn.charset, function(x) { 154 | return '\\u' + ('0000' + x.charCodeAt(0).toString(16)).substr(-4); 155 | }); 156 | var re = new RegExp('^[' + chars.join('') + ']+$'); 157 | if (!re.test(fn(256))) { 158 | throw new Error('Bad chars for ' + fn.charset); 159 | } 160 | }, 161 | entropy: function(fn) { 162 | var len = fn.entropy(128).length; 163 | if (len * (Math.log(fn.charset.length) / Math.LN2) < 128) { 164 | throw new Error('Wrong length for entropy: ' + len); 165 | } 166 | }, 167 | uniqueness: function(fn, quick) { 168 | var uniq = {}; 169 | for (var i = 0; i < (quick ? 10 : 10000); i++) { 170 | var s = fn(); 171 | if (uniq[s]) { 172 | throw new Error('Repeated result: ' + s); 173 | } 174 | uniq[s] = true; 175 | } 176 | }, 177 | bias: function(fn, quick) { 178 | if (quick) return; 179 | var s = '', counts = {}; 180 | for (var i = 0; i < 1000; i++) { 181 | s += fn(1000); 182 | } 183 | for (i = 0; i < s.length; i++) { 184 | var c = s.charAt(i); 185 | counts[c] = (counts[c] || 0) + 1; 186 | } 187 | var avg = s.length / fn.charset.length; 188 | for (var k in counts) { 189 | var diff = counts[k] / avg; 190 | if (diff < 0.95 || diff > 1.05) { 191 | throw new Error('Biased "' + k + '": average is ' + avg + 192 | ", got " + counts[k] + ' in ' + fn.charset); 193 | } 194 | } 195 | } 196 | }; 197 | 198 | randomString.test = function(quick) { 199 | for(var test in TESTS) { 200 | var t = TESTS[test]; 201 | t(randomString, quick); 202 | t(randomString.custom('abc'), quick); 203 | for (var cname in CHARSETS) { 204 | t(randomString[cname], quick); 205 | } 206 | } 207 | }; 208 | 209 | return randomString; 210 | 211 | }()); 212 | -------------------------------------------------------------------------------- /src/core.css: -------------------------------------------------------------------------------- 1 | /* App layout */ 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | 8 | body { 9 | display: flex; 10 | flex-flow: column; 11 | color: #fff; 12 | font: normal 14px Roboto, sans-serif; 13 | background-color: #282a36; 14 | } 15 | 16 | #editor { 17 | flex-grow: 1; 18 | margin-top: 0.4em; 19 | overflow: auto; 20 | } 21 | 22 | footer { 23 | background-color: #3b3b47 !important; 24 | } 25 | 26 | #controls, 27 | #copy { 28 | background-color: #282a36; 29 | } 30 | 31 | #controls, 32 | #copy, 33 | footer { 34 | z-index: 10; 35 | } 36 | 37 | #progress { 38 | min-height: 3px; 39 | background: #ff79c6; 40 | z-index: 15; 41 | width: 0; 42 | } 43 | 44 | .hidden, 45 | select, 46 | #copy:not(.hidden)+#controls, 47 | body.readonly .hide-readonly, 48 | body:not(.readonly) .show-readonly, 49 | body.readonly:not(:hover) #footer { 50 | display: none; 51 | } 52 | 53 | #copy-link { 54 | font-family: JetBrainsMono, sans-serif; 55 | width: 100%; 56 | } 57 | 58 | /* Styling */ 59 | .shadow-bottom { 60 | box-shadow: rgba(0, 0, 0, 0.15) 0 3px 10px; 61 | } 62 | 63 | .shadow-top { 64 | box-shadow: rgba(0, 0, 0, 0.15) 0 -3px 10px; 65 | } 66 | 67 | a, 68 | a:hover, 69 | a:active, 70 | a:focus { 71 | color: #fff; 72 | outline: none; 73 | } 74 | 75 | #controls a, 76 | #footer a { 77 | text-decoration: none; 78 | } 79 | 80 | #controls a:hover { 81 | border-bottom: 1px solid rgba(255, 255, 255, 0.5); 82 | } 83 | 84 | .CodeMirror { 85 | height: 100%; 86 | font-family: JetBrainsMono, sans-serif; 87 | } 88 | 89 | h1 { 90 | font: normal 22px JetBrainsMono, sans-serif; 91 | white-space: nowrap; 92 | } 93 | 94 | .mono { 95 | font-family: JetBrainsMono, sans-serif; 96 | } 97 | 98 | .pink { 99 | color: #ff79c6; 100 | } 101 | 102 | /* Modals */ 103 | 104 | .modal { 105 | display: none; 106 | } 107 | 108 | .modal.is-open { 109 | display: block; 110 | } 111 | 112 | .modal-content { 113 | background-color: #3b3b47; 114 | max-width: 600px; 115 | max-height: 100vh; 116 | overflow-y: auto; 117 | box-sizing: border-box; 118 | } 119 | 120 | .modal-overlay { 121 | position: fixed; 122 | background: rgba(0, 0, 0, 0.2); 123 | right: 0; 124 | top: 0; 125 | left: 0; 126 | bottom: 0; 127 | z-index: 20; 128 | display: flex; 129 | justify-content: center; 130 | align-items: center; 131 | } 132 | 133 | /* Form elements */ 134 | 135 | #controls .ss-main { 136 | width: 180px; 137 | } 138 | 139 | .ss-main .ss-single-selected, 140 | button, 141 | input[type='text'], 142 | input[type='search'] { 143 | background-color: #3b3b47 !important; 144 | color: #fff !important; 145 | border-radius: 2px !important; 146 | border: 1px solid #ccc !important; 147 | font: normal 14px Roboto, sans-serif; 148 | height: 26px !important; 149 | } 150 | 151 | input::-webkit-search-cancel-button { 152 | display: none; 153 | } 154 | 155 | input::-moz-selection { 156 | background-color: rgba(90, 95, 128, 0.99); 157 | } 158 | 159 | input::selection { 160 | background-color: rgba(90, 95, 128, 0.99); 161 | } 162 | 163 | button { 164 | cursor: pointer; 165 | } 166 | 167 | button:hover { 168 | background-color: rgba(255, 255, 255, 0.1) !important; 169 | } 170 | 171 | .ss-content { 172 | background-color: #282936; 173 | color: #dedede; 174 | font-size: 14px; 175 | } 176 | 177 | .ss-content .ss-disabled { 178 | background-color: #3b3b47 !important; 179 | } 180 | 181 | /* Fonts */ 182 | @font-face { 183 | font-family: 'JetBrainsMono'; 184 | src: url('../static/JetBrainsMono-Regular.woff2') format('woff2'), 185 | url('../static/JetBrainsMono-Regular.woff') format('woff'), 186 | url('../static/JetBrainsMono-Regular.ttf') format('truetype'); 187 | font-weight: 400; 188 | font-style: normal; 189 | } 190 | 191 | @font-face { 192 | font-family: 'Roboto'; 193 | font-style: normal; 194 | font-weight: 400; 195 | font-display: swap; 196 | src: local('Roboto'), local('Roboto-Regular'), url('../static/KFOmCnqEu92Fr1Mu4mxK.woff2') format('woff2'); 197 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, 198 | U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 199 | } 200 | 201 | /* Icons */ 202 | 203 | @font-face { 204 | font-family: 'icomoon'; 205 | src: url('data:application/x-font-woff;charset=utf-8;base64,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') format('truetype'); 206 | font-weight: normal; 207 | font-style: normal; 208 | font-display: block; 209 | } 210 | 211 | [class^='icon-'], 212 | [class*=' icon-'] { 213 | font-family: 'icomoon' !important; 214 | speak: none; 215 | font-style: normal; 216 | font-weight: normal; 217 | font-variant: normal; 218 | text-transform: none; 219 | line-height: 1; 220 | -webkit-font-smoothing: antialiased; 221 | -moz-osx-font-smoothing: grayscale; 222 | font-size: 21px; 223 | } 224 | 225 | .icon-edit { 226 | font-size: 12px; 227 | } 228 | 229 | .icon-question:before { 230 | content: '\e900'; 231 | } 232 | 233 | .icon-github:before { 234 | content: '\e901'; 235 | } 236 | 237 | .icon-edit:before { 238 | content: '\e903'; 239 | } 240 | 241 | .icon-notes:before { 242 | content: '\e902'; 243 | } 244 | 245 | .icon-wrap-text:before { 246 | content: '\e905'; 247 | } 248 | 249 | a#version-number { 250 | text-decoration: none; 251 | } 252 | 253 | /* CSS for Log in with SkyID Button */ 254 | .skyid-button, 255 | .skyid-button:focus { 256 | color: #fff; 257 | background-color: #57b560; 258 | border-radius: .25rem; 259 | font-weight: 700; 260 | border: none; 261 | cursor: pointer; 262 | outline: none; 263 | } 264 | 265 | .skyid-logo { 266 | height: 16px; 267 | width: 16px; 268 | vertical-align: middle; 269 | margin-right: 2px; 270 | } 271 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Add to Homescreen](https://img.shields.io/badge/Skynet-Add%20To%20Homescreen-00c65e?style=for-the-badge&labelColor=0d0d0d&logo=data:image/png;base64,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)](https://homescreen.hns.siasky.net/#/skylink/AQDn5uriOXZFLumP0QhJUO7D3kPkJ2SPciJCiShDnXb1Dw) 2 | 3 | # **[Hacker Paste](https://hackerpaste.hns.siasky.net)** 4 | ...is a one-of-a-kind text snippet sharing tool, secure by design and built for the decentralized web. 5 | 6 | ## The best paste bin ever created 7 | * Hacker Paste is **entirely in-browser**. There is no central server. Your data is not submitted to any intermediary processors. Everything that goes on in the app can be viewed in your browser's inspector. The app and its operations are highly transparent. 8 | * Hacker Paste stores snippets to [Skynet](https://siasky.net), a decentralized content delivery network. All pastes generated in Hacker Paste, and the Hacker Paste app itself, can be **accessed on any Skynet portal.** 9 | * Hacker Paste **encrypts all data** with AES encryption prior to being uploaded, using a securely generated encryption key that is kept in the snippet URL. The existence of this document is not recorded anywhere other than your list of saved pastes. The existence of the document and its contents are only known to you and anyone you share the URL with. 10 | * **Syntax highlighting** in any programming language you can think of 11 | 12 | ## See for yourself 13 | * [Example document](https://hackerpaste.hns.siasky.net/#AAB0AzZ2_C2-lM9IFRVeP9-rzJHNrTEvEMuG2mg7ri4ZrQIOdMV8pl2h8XtEMuMeIN) 14 | * [The same document but accessed through a different portal](https://hackerpaste.hns.skyportal.xyz/#AAB0AzZ2_C2-lM9IFRVeP9-rzJHNrTEvEMuG2mg7ri4ZrQIOdMV8pl2h8XtEMuMeIN) 15 | * [Raw, encrypted text stored on Skynet](https://siasky.net/AAB0AzZ2_C2-lM9IFRVeP9-rzJHNrTEvEMuG2mg7ri4ZrQ) 16 | 17 | ## Why this matters 18 | * Share documents privately and discreetly with anyone with just a URL, no complicated encryption tools required. 19 | * Decentralized storage and delivery means resistence to censorship and deplatforming. While public portals like [siasky.net](https://siasky.net) and [skyportal.xyz](https://skyportal.xyz) are available for convenience, anyone can operate their own Skynet portal and use Hacker Paste that way. 20 | * No central app server means the app can't go down. 21 | 22 | ## Usage 23 | 24 | No user account is needed to use Hacker Paste. However, you can optionally sign in with [SkyID](https://sky-id.hns.siasky.net/). This allows you to save your pastes to your "My Pastes" list and to generate and save pastes at reusable URLs (i.e., mutable documents). No personal information of any kind is needed to create a SkyID account. 25 | 26 | Both logged in and logged out accounts can save "Snapshots," which are immutable documents. Updating the document generates a new link. 27 | 28 | ## Build 29 | 30 | 1. `git clone https://github.com/harej/hackerpaste && cd hackerpaste` 31 | 2. `npm install` 32 | 3. `npm run build` 33 | 34 | ## Deploy to Skynet 35 | 36 | Anything pushed to the `main` branch of `https://github.com/harej/hackerpaste` will be automatically deployed to Skynet and made available through the Handshake domain `hackerpaste.hns` and the Ethereum domain `hackerpaste.eth`. The `main` branch should be considered the source of truth as to the latest version of Hacker Paste. 37 | 38 | If you would like to deploy your own build of Hacker Paste to Skynet (either because you've made modifications or you do not trust the owner of the `hackerpaste.hns` or `hackerpaste.eth` domains), upload the `build` directory to Skynet using a portal such as [siasky.net](https://siasky.net). Be sure to select the directory upload option. 39 | 40 | Once you upload the app you will get a link with a 55-character subdomain to your deployment. This link will always point to that exact version of the app. (In certain scenarios, you may prefer a link that can be guaranteed to load the same version of the app.) If you want to be able to update the app and not have to share new links, it is recommended you create a "resolver skylink" using [Rift](https://riftapp.hns.siasky.net/#/). To do so: 41 | 42 | 1. Log in with Skynet (MySky). If you do not have a MySky account, you will be prompted to create one. (Note this is different from SkyID, the predecessor login system used by Hacker Paste.) 43 | 2. From the row of menu options select "DNS" 44 | 3. Click the "Add DNS record" button on the top right. You will be prompted for a name and a skylink. If you have a HNS or ENS name for the app you can use that, or you can use any arbitrary identifying label. For the skylink, take the 55-character app subdomain and somehow convert it into a 46-character skylink. 45 | 4. Click "Save". You will have a resolver skylink that can be updated to point to the latest version of Hacker Paste. 46 | 47 | There are tutorials available for [associating resolver links with Handshake](https://docs.siasky.net/integrations/hns-names) and [associating resolver links with Ethereum Name Service](https://docs.siasky.net/integrations/ens-ethereum-name-service). 48 | 49 | ## Donate 50 | 51 | Check out [Hacker Paste on Gitcoin](https://gitcoin.co/grants/3094/hacker-paste). 52 | 53 | Alternatively you can send SiaCoin to `4868b7b041f0b80b61b02a07032454667ea69b0f909ea289a016b5678307d033542de76d28a9`. 54 | -------------------------------------------------------------------------------- /src/editor.js: -------------------------------------------------------------------------------- 1 | /*jshint esversion: 8 */ 2 | 3 | import CodeMirror from 'codemirror'; 4 | import SlimSelect from 'slim-select'; 5 | import marked from 'marked'; 6 | 7 | import { skyid, pubkey, startSkyIDSession, switchToLoggedOut } 8 | from './account.js'; 9 | import { decryptData, encryptObject } 10 | from './encryption.js'; 11 | import { byId, byClass, hideCopyBar, hideCopyBarNow, clickListener} 12 | from './interface.js'; 13 | import { buildUrl } 14 | from './links.js'; 15 | import { shorten, base64ToHex } 16 | from './utility.js'; 17 | import { generateEmbed, generateSnapshotUrl, generatePersistentUrl } 18 | from './links.js'; 19 | 20 | import 'codemirror/lib/codemirror.css'; 21 | import 'slim-select/dist/slimselect.min.css'; 22 | import 'codemirror/addon/scroll/simplescrollbars.css'; 23 | import 'codemirror/theme/dracula.css'; 24 | 25 | import 'codemirror/addon/mode/loadmode'; 26 | import 'codemirror/addon/mode/overlay'; 27 | import 'codemirror/addon/mode/multiplex'; 28 | import 'codemirror/addon/mode/simple'; 29 | import 'codemirror/addon/scroll/simplescrollbars'; 30 | import 'codemirror/mode/meta'; 31 | import 'codemirror/mode/apl/apl'; 32 | import 'codemirror/mode/asciiarmor/asciiarmor'; 33 | import 'codemirror/mode/asn.1/asn.1'; 34 | import 'codemirror/mode/asterisk/asterisk'; 35 | import 'codemirror/mode/brainfuck/brainfuck'; 36 | import 'codemirror/mode/clike/clike'; 37 | import 'codemirror/mode/clojure/clojure'; 38 | import 'codemirror/mode/cmake/cmake'; 39 | import 'codemirror/mode/cobol/cobol'; 40 | import 'codemirror/mode/coffeescript/coffeescript'; 41 | import 'codemirror/mode/commonlisp/commonlisp'; 42 | import 'codemirror/mode/crystal/crystal'; 43 | import 'codemirror/mode/css/css'; 44 | import 'codemirror/mode/cypher/cypher'; 45 | import 'codemirror/mode/d/d'; 46 | import 'codemirror/mode/dart/dart'; 47 | import 'codemirror/mode/diff/diff'; 48 | import 'codemirror/mode/django/django'; 49 | import 'codemirror/mode/dockerfile/dockerfile'; 50 | import 'codemirror/mode/dtd/dtd'; 51 | import 'codemirror/mode/dylan/dylan'; 52 | import 'codemirror/mode/ebnf/ebnf'; 53 | import 'codemirror/mode/ecl/ecl'; 54 | import 'codemirror/mode/eiffel/eiffel'; 55 | import 'codemirror/mode/elm/elm'; 56 | import 'codemirror/mode/erlang/erlang'; 57 | import 'codemirror/mode/factor/factor'; 58 | import 'codemirror/mode/fcl/fcl'; 59 | import 'codemirror/mode/forth/forth'; 60 | import 'codemirror/mode/fortran/fortran'; 61 | import 'codemirror/mode/gas/gas'; 62 | import 'codemirror/mode/gfm/gfm'; 63 | import 'codemirror/mode/gherkin/gherkin'; 64 | import 'codemirror/mode/go/go'; 65 | import 'codemirror/mode/groovy/groovy'; 66 | import 'codemirror/mode/haml/haml'; 67 | import 'codemirror/mode/handlebars/handlebars'; 68 | import 'codemirror/mode/haskell/haskell'; 69 | import 'codemirror/mode/haskell-literate/haskell-literate'; 70 | import 'codemirror/mode/haxe/haxe'; 71 | import 'codemirror/mode/htmlembedded/htmlembedded'; 72 | import 'codemirror/mode/htmlmixed/htmlmixed'; 73 | import 'codemirror/mode/http/http'; 74 | import 'codemirror/mode/idl/idl'; 75 | import 'codemirror/mode/javascript/javascript'; 76 | import 'codemirror/mode/jinja2/jinja2'; 77 | import 'codemirror/mode/jsx/jsx'; 78 | import 'codemirror/mode/julia/julia'; 79 | import 'codemirror/mode/livescript/livescript'; 80 | import 'codemirror/mode/lua/lua'; 81 | import 'codemirror/mode/markdown/markdown'; 82 | import 'codemirror/mode/mathematica/mathematica'; 83 | import 'codemirror/mode/mbox/mbox'; 84 | import 'codemirror/mode/mirc/mirc'; 85 | import 'codemirror/mode/mllike/mllike'; 86 | import 'codemirror/mode/modelica/modelica'; 87 | import 'codemirror/mode/mscgen/mscgen'; 88 | import 'codemirror/mode/mumps/mumps'; 89 | import 'codemirror/mode/nginx/nginx'; 90 | import 'codemirror/mode/nsis/nsis'; 91 | import 'codemirror/mode/ntriples/ntriples'; 92 | import 'codemirror/mode/octave/octave'; 93 | import 'codemirror/mode/oz/oz'; 94 | import 'codemirror/mode/pascal/pascal'; 95 | import 'codemirror/mode/pegjs/pegjs'; 96 | import 'codemirror/mode/perl/perl'; 97 | import 'codemirror/mode/php/php'; 98 | import 'codemirror/mode/pig/pig'; 99 | import 'codemirror/mode/powershell/powershell'; 100 | import 'codemirror/mode/properties/properties'; 101 | import 'codemirror/mode/protobuf/protobuf'; 102 | import 'codemirror/mode/pug/pug'; 103 | import 'codemirror/mode/puppet/puppet'; 104 | import 'codemirror/mode/python/python'; 105 | import 'codemirror/mode/q/q'; 106 | import 'codemirror/mode/r/r'; 107 | import 'codemirror/mode/rpm/rpm'; 108 | import 'codemirror/mode/rst/rst'; 109 | import 'codemirror/mode/ruby/ruby'; 110 | import 'codemirror/mode/rust/rust'; 111 | import 'codemirror/mode/sas/sas'; 112 | import 'codemirror/mode/sass/sass'; 113 | import 'codemirror/mode/scheme/scheme'; 114 | import 'codemirror/mode/shell/shell'; 115 | import 'codemirror/mode/sieve/sieve'; 116 | import 'codemirror/mode/slim/slim'; 117 | import 'codemirror/mode/smalltalk/smalltalk'; 118 | import 'codemirror/mode/smarty/smarty'; 119 | import 'codemirror/mode/solr/solr'; 120 | import 'codemirror/mode/soy/soy'; 121 | import 'codemirror/mode/sparql/sparql'; 122 | import 'codemirror/mode/spreadsheet/spreadsheet'; 123 | import 'codemirror/mode/sql/sql'; 124 | import 'codemirror/mode/stex/stex'; 125 | import 'codemirror/mode/stylus/stylus'; 126 | import 'codemirror/mode/swift/swift'; 127 | import 'codemirror/mode/tcl/tcl'; 128 | import 'codemirror/mode/textile/textile'; 129 | import 'codemirror/mode/tiddlywiki/tiddlywiki'; 130 | import 'codemirror/mode/tiki/tiki'; 131 | import 'codemirror/mode/toml/toml'; 132 | import 'codemirror/mode/tornado/tornado'; 133 | import 'codemirror/mode/troff/troff'; 134 | import 'codemirror/mode/ttcn/ttcn'; 135 | import 'codemirror/mode/ttcn-cfg/ttcn-cfg'; 136 | import 'codemirror/mode/turtle/turtle'; 137 | import 'codemirror/mode/twig/twig'; 138 | import 'codemirror/mode/vb/vb'; 139 | import 'codemirror/mode/vbscript/vbscript'; 140 | import 'codemirror/mode/velocity/velocity'; 141 | import 'codemirror/mode/verilog/verilog'; 142 | import 'codemirror/mode/vhdl/vhdl'; 143 | import 'codemirror/mode/vue/vue'; 144 | import 'codemirror/mode/wast/wast'; 145 | import 'codemirror/mode/webidl/webidl'; 146 | import 'codemirror/mode/xml/xml'; 147 | import 'codemirror/mode/xquery/xquery'; 148 | import 'codemirror/mode/yacas/yacas'; 149 | import 'codemirror/mode/yaml/yaml'; 150 | import 'codemirror/mode/yaml-frontmatter/yaml-frontmatter'; 151 | import 'codemirror/mode/z80/z80'; 152 | 153 | export var editor; 154 | export var select; 155 | export var myPastes; 156 | export var docLabel; 157 | export var persistentDocKey; 158 | export var statsEl; 159 | 160 | export const initCode = () => { 161 | let payload = location.hash.substr(1); 162 | if (payload.length === 0) return; 163 | payload = payload.split("."); 164 | let docID = payload[0]; 165 | loadByDocID(docID); 166 | }; 167 | 168 | export const initCodeEditor = () => { 169 | persistentDocKey = null; 170 | docLabel = null; 171 | editor = new CodeMirror(byId("editor"), { 172 | lineNumbers: true, 173 | theme: "dracula", 174 | readOnly: readOnly, 175 | lineWrapping: true, 176 | scrollbarStyle: "simple", 177 | }); 178 | if (readOnly) { 179 | document.body.classList.add("readonly"); 180 | } 181 | statsEl = byId("stats"); 182 | editor.on("change", () => { 183 | statsEl.innerHTML = `Length: ${editor.getValue().length} | Lines: ${ 184 | editor.doc.size 185 | }`; 186 | hideCopyBar(); 187 | }); 188 | }; 189 | 190 | export const initLangSelector = () => { 191 | select = new SlimSelect({ 192 | select: "#language", 193 | data: CodeMirror.modeInfo.map((e) => ({ 194 | text: e.name, 195 | value: shorten(e.name), 196 | data: { 197 | mime: e.mime, 198 | mode: e.mode 199 | }, 200 | })), 201 | showContent: "down", 202 | onChange: (e) => { 203 | const language = e.data || { 204 | mime: null, 205 | mode: null 206 | }; 207 | editor.setOption("mode", language.mime); 208 | CodeMirror.autoLoadMode(editor, language.mode); 209 | document.title = 210 | e.text && e.text !== "Plain Text" ? 211 | `Hacker Paste - ${e.text} code snippet` : 212 | "Hacker Paste"; 213 | }, 214 | }); 215 | 216 | // Set lang selector 217 | const l = location.hash.substr(1).split(".")[1] || 218 | new URLSearchParams(window.location.search).get("l"); 219 | select.set(l ? decodeURIComponent(l) : shorten("Plain Text")); 220 | }; 221 | 222 | export const initKeyboardShortcuts = () => { 223 | // Save snapshot when you press CTRL+S 224 | document.addEventListener("keydown", (event) => { 225 | if (event.key === "s" && event.ctrlKey) { 226 | event.preventDefault(); 227 | generateSnapshotUrl(); 228 | } 229 | }); 230 | }; 231 | 232 | export const initListeners = () => { 233 | clickListener("copy-close-button", hideCopyBarNow); 234 | clickListener("wordmark", backToEditor); 235 | clickListener("button-username", startSkyIDSession); 236 | clickListener("button-log-out", switchToLoggedOut); 237 | clickListener("enable-line-wrapping", enableLineWrapping); 238 | clickListener("disable-line-wrapping", disableLineWrapping); 239 | clickListener("embed-button", generateEmbed); 240 | clickListener("save-snapshot-button", generateSnapshotUrl); 241 | clickListener("save-to-my-pastes-button", generatePersistentUrl); 242 | }; 243 | 244 | const loadByDocID = (docID) => { 245 | // A `docID` is a document's retrieval string plus its decryption string. 246 | // A file suffix for syntax highlighting does not count as part of the docID. 247 | // Different varieties of docID can be distinguished by their lengths: 248 | // 249 | // * 46 characters: unencrypted, immutable files identified by skylink (as 250 | // produced by very early versions of Hacker Paste). Hacker Paste will 251 | // open all valid skylinks that are text-based files. 252 | // * 64 characters: encrypted, mutable SkyDB files. The skylink representing 253 | // the latest version is posted to the SkyDB entry, with the user's public 254 | // key, a 64-character hex string, padded with '00' at the end and 255 | // re-encoded as a 44-character base64 string. The following 20 characters 256 | // are used to decrypt the file. These are "my pastes" links. 257 | // * 66 characters: encrypted, immutable skyfiles. The first 46 characters 258 | // identifies the skyfile while the remaining 20 characters decrypt the 259 | // file. These are "snapshot" links. 260 | 261 | var skylink; 262 | var docKey; 263 | 264 | persistentDocKey = null; 265 | docLabel = null; 266 | 267 | if (docID.length === 46) loadSkylink(docID); 268 | else if (docID.length === 66) { 269 | docKey = docID.substr(46); 270 | skylink = docID.substr(0, 46); 271 | loadSkylink(skylink, docKey); 272 | } else if (docID.length == 64) { 273 | docKey = docID.substr(44); 274 | persistentDocKey = docKey; 275 | let docPubkey = base64ToHex(docID.substr(0, 44)).substr(0, 64); 276 | skyid.skynetClient.registry.getEntry(docPubkey, 277 | `hackerpaste:file:${docKey}`) 278 | .then((result) => { 279 | skylink = result.entry.data; 280 | loadSkylink(skylink, docKey); 281 | }) 282 | .catch((error) => { 283 | console.error(error); 284 | }); 285 | } else alert('This is not a valid paste link.'); 286 | }; 287 | 288 | const loadView = (viewContent) => { 289 | editor.setOption('readOnly', true); 290 | byClass("CodeMirror-cursors").setAttribute("style", "display:none;"); 291 | byClass("CodeMirror-code").innerHTML = 292 | `
${viewContent}
`; 293 | }; 294 | 295 | async function fetchSkylink(skylink) { 296 | let content = skyid.skynetClient.getFileContent(skylink); 297 | return content; 298 | } 299 | 300 | const loadSkylink = (skylink, docKey) => { 301 | skyid.skynetClient.getFileContent(skylink) 302 | .then((data) => { 303 | data = data.data; // drop the metadata; get to the good stuff 304 | let loadAsMarkdown = false; 305 | if (docKey) data = decryptData(data, docKey); 306 | if (loadAsMarkdown) { 307 | loadView(marked(data)); 308 | } else editor.setValue(data); 309 | }) 310 | .catch((error) => { 311 | console.error("Error:", error); 312 | }); 313 | }; 314 | 315 | export const backToEditor = () => { 316 | byId("editor").innerHTML = ""; 317 | initCodeEditor(); 318 | }; 319 | 320 | export const loadMyPastes = () => { 321 | let view = "
"; 329 | view += ``; 330 | loadView(view); 331 | clickListener("new-paste-button", backToEditor); 332 | }; 333 | 334 | export async function updateMyPastes(docID, docLabel) { 335 | myPastes.documents.push({label: docLabel, docID: docID}); 336 | let newPasteList = encryptObject(myPastes, skyid.seed); 337 | skyid.setJSON('hackerpaste:my-pastes', newPasteList, (response) => { 338 | if (response !== true) console.error(response); 339 | }) 340 | } 341 | 342 | export const disableLineWrapping = () => { 343 | byId("disable-line-wrapping").classList.add("hidden"); 344 | byId("enable-line-wrapping").classList.remove("hidden"); 345 | editor.setOption("lineWrapping", false); 346 | }; 347 | 348 | export const enableLineWrapping = () => { 349 | byId("enable-line-wrapping").classList.add("hidden"); 350 | byId("disable-line-wrapping").classList.remove("hidden"); 351 | editor.setOption("lineWrapping", true); 352 | }; 353 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 13 | 15 | 17 | 18 | Hacker Paste – The Secure and Decentralized Paste Bin 19 | 20 | 23 | 25 | 26 | 27 | 28 | 29 | 38 | 39 | 56 | 57 |
58 |
59 |
60 |

Hacker Paste 61 |

62 |
63 | 64 |
65 | 73 | 76 | $ 77 |
78 | 79 |
80 | 81 |
82 | 84 |
85 | 86 | 87 |
88 | 92 | 96 |
97 | 98 |
99 | 106 | 113 | 119 |
120 |
121 |
122 | 123 |
124 |
125 | 126 |
127 |
128 | 129 | 150 | 151 | 158 | 159 | 160 | 163 | 164 | 165 | 166 | --------------------------------------------------------------------------------