409 lines
13 KiB
JavaScript
409 lines
13 KiB
JavaScript
import React, { Component } from "react";
|
|
import { Form, Button, Alert } from "bootstrap-4-react";
|
|
|
|
import "./paste.css";
|
|
|
|
import "highlight.js/styles/github.css";
|
|
import Highlight from "react-highlight";
|
|
import Markdown from 'react-markdown';
|
|
|
|
import pako from "pako";
|
|
import seedrandom from "seedrandom";
|
|
import { remoteService } from "./RemoteService"
|
|
|
|
import RIPEMD160 from "crypto-js/ripemd160";
|
|
import SHA256 from "crypto-js/sha256";
|
|
import AES from "crypto-js/aes";
|
|
import BASE64 from "crypto-js/enc-base64";
|
|
import HEX from "crypto-js/enc-hex";
|
|
import UTF8 from "crypto-js/enc-utf8"
|
|
|
|
const rng = seedrandom();
|
|
const req = remoteService();
|
|
|
|
const b64 = (s) => (!!s?BASE64.stringify(s).replace(/[=]+/, '').replace(/\//g, '_').replace(/\+/g, '-'):'');
|
|
const sha = (s) => b64(SHA256(s));
|
|
const chk = (s) => b64(RIPEMD160(SHA256(s)));
|
|
const enc = (t, p) => AES.encrypt(t, p).toString();
|
|
const u16a = (ua) => {
|
|
let s = '';
|
|
for (let i = 0; i < ua.length; i++) {
|
|
s += ('0' + ua[i].toString(16)).slice(-2);
|
|
}
|
|
return HEX.parse(s);
|
|
};
|
|
const u8a = (wa) => {
|
|
const w = wa.words;
|
|
let b = new Uint8Array(w.length * 4), v, i, j, k = 0;
|
|
for (i = 0; i < w.length; ++i) {
|
|
v = w[i];
|
|
for (j = 3; j >= 0; --j) {
|
|
b[k++] = ((v >> 8 * j) & 0xFF);
|
|
}
|
|
}
|
|
return b;
|
|
};
|
|
const str8 = (ua) => {
|
|
let s = '';
|
|
for (let i = 0; i < ua.byteLength; i++) {
|
|
if ((ua[i]&0x80) === 0) s += String.fromCharCode(ua[i]);
|
|
else if ((ua[i]&0xe0) === 0xc0 && (ua[i+1]&0xc0) === 0x80) {
|
|
s += String.fromCharCode(((ua[i]&0x1f)<<6) + (ua[i+1]&0x3f));
|
|
i += 1;
|
|
}
|
|
else if ((ua[i]&0xf0) === 0xe0 && (ua[i+1]&0xc0) === 0x80 && (ua[i+2]&0xc0) === 0x80){
|
|
s += String.fromCharCode(((ua[i]&0x0f)<<12) + ((ua[i+1]&0x3f)<<6) + (ua[i+2]&0x3f));
|
|
i += 2;
|
|
}
|
|
else if ((ua[i]&0xf8) === 0xf0 && (ua[i+1]&0xc0) === 0x80 && (ua[i+2]&0xc0) === 0x80 && (ua[i+3]&0xc0) === 0x80) {
|
|
s += String.fromCharCode(((ua[i]&0x0f)<<18) + ((ua[i+1]&0x3f)<<12) + ((ua[i+2]&0x3f)<<6) + (ua[i+3]&0x3f));
|
|
i += 3;
|
|
}
|
|
else { s += String.fromCharCode(65533); }
|
|
}
|
|
return s;
|
|
};
|
|
const zip = (text) => u16a(pako.gzip(text));
|
|
const dec = (c, p, z) => {
|
|
if (z) {
|
|
let tx = AES.decrypt(c, p);
|
|
tx = u8a(tx);
|
|
return str8(pako.inflate(tx));
|
|
} else {
|
|
return AES.decrypt(c, p).toString(UTF8);
|
|
}
|
|
}
|
|
const blength = (o) => {
|
|
if (!(typeof o === 'string' || o instanceof String)) return 0;
|
|
if (o === undefined || o.length === undefined) return 0;
|
|
var utf8length = 0;
|
|
for (var n = 0; n < o.length; n++) {
|
|
var c = o.charCodeAt(n);
|
|
if (c < 128) {
|
|
utf8length++;
|
|
}
|
|
else if ((c > 127) && (c < 2048)) {
|
|
utf8length = utf8length + 2;
|
|
} else {
|
|
utf8length = utf8length + 3;
|
|
}
|
|
}
|
|
return utf8length;
|
|
};
|
|
|
|
const PASTE_API = (''+window.location).replace(/ui\/.*$/, 'paste');
|
|
|
|
const syntaxItems = [
|
|
["text", "Plain Text"],
|
|
["markdown", "Markdown"],
|
|
|
|
["apache", "Apache"],
|
|
["bash", "Bash"],
|
|
["coffeescript", "CoffeeScript"],
|
|
["cpp", "C++"],
|
|
["cs", "C#"],
|
|
["css", "CSS"],
|
|
["diff", "Diff"],
|
|
["http", "HTTP"],
|
|
["ini", "Ini"],
|
|
["java", "Java"],
|
|
["javascript", "JavaScript"],
|
|
["json", "JSON"],
|
|
["makefile", "Makefile"],
|
|
["nginx", "Nginx"],
|
|
["objectivec", "Objective C"],
|
|
["perl", "Perl"],
|
|
["php", "PHP"],
|
|
["python", "Python"],
|
|
["ruby", "Ruby"],
|
|
["sql", "SQL"],
|
|
["xml", "HTML, XML"]
|
|
];
|
|
const expireItems = [
|
|
[3600, "1 Hour"],
|
|
[86400, "1 Day"],
|
|
[604800, "1 Week"],
|
|
[2419200, "4 Weeks"],
|
|
[15778463, "6 Months"],
|
|
[31556926, "1 Year"]
|
|
];
|
|
|
|
|
|
class Paste extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
const [ hash, key ] = window.location.hash.substring(2).split("!");
|
|
|
|
this.state = {
|
|
error: "",
|
|
syntax: "text",
|
|
expire: 604800,
|
|
expires: "",
|
|
burn: false,
|
|
plain: "",
|
|
cipher: "",
|
|
decryptKey: hash !== "new" ? key : "",
|
|
hash: hash !== "new" ? hash : "",
|
|
entropy: 0,
|
|
gzip: false,
|
|
syntaxItems,
|
|
expireItems
|
|
};
|
|
|
|
this.startEntropy = this.startEntropy.bind(this);
|
|
this.addEntropy = this.addEntropy.bind(this);
|
|
this.onChange = this.onChange.bind(this);
|
|
this.onSubmit = this.onSubmit.bind(this);
|
|
this.onNew = this.onNew.bind(this);
|
|
this.onCopy = this.onCopy.bind(this);
|
|
this.encrypt = this.encrypt.bind(this);
|
|
this.decrypt = this.decrypt.bind(this);
|
|
}
|
|
|
|
startEntropy(events, count) {
|
|
let t = [];
|
|
|
|
let fn = (e) => {
|
|
t.push([e.pageX, e.pageY, e.keyCode, +new Date()]);
|
|
if (t.length < count) {
|
|
return;
|
|
}
|
|
this.addEntropy(t);
|
|
t = [];
|
|
};
|
|
fn = fn.bind(this);
|
|
|
|
for (let i in events) {
|
|
if (events.hasOwnProperty(i))
|
|
document.addEventListener(events[i], fn);
|
|
}
|
|
};
|
|
|
|
addEntropy(s) {
|
|
this.setState(function(state, props) {
|
|
return {entropy: state.entropy + s.length};
|
|
} );
|
|
seedrandom(s, {entropy: true});
|
|
};
|
|
|
|
onChange(event) {
|
|
const target = event.target;
|
|
const value = target.type === 'checkbox' ? target.checked : target.value;
|
|
const name = target.name;
|
|
|
|
this.setState({
|
|
[name]: value
|
|
});
|
|
}
|
|
|
|
onSubmit(event) {
|
|
event.preventDefault();
|
|
const { plain } = this.state;
|
|
// const { history } = this.props;
|
|
|
|
this.encrypt(plain).then(([hash, decryptKey]) => { history.pushState({}, '', '/ui/#/' + hash + '!' + decryptKey) });
|
|
}
|
|
onNew(event) {
|
|
// const { history } = this.props;
|
|
this.setState({cipher:"", plain:"", hash: "", decryptKey: "", syntax: "text", expire: 604800, burn: false}, () => history.pushState({}, '', '/ui'));
|
|
}
|
|
onCopy(event) {
|
|
// const { history } = this.props;
|
|
this.setState({hash: "", decryptKey: ""}, () => history.pushState({}, '', '/ui'))
|
|
}
|
|
|
|
decrypt(tx) {
|
|
if (tx === "") {
|
|
this.setState({hash: "", decryptKey: "", error: "Unable to retrieve paste."})
|
|
return;
|
|
}
|
|
|
|
let s = tx.split('\n');
|
|
let i = 0;
|
|
|
|
let header = {};
|
|
while (true) {
|
|
if (s[i] === "") break;
|
|
|
|
var l = s[i].trim().split(':\t');
|
|
header[l[0]] = l[1];
|
|
|
|
i++;
|
|
}
|
|
const cipher = s.splice(i).join('');
|
|
const zip = !!header.zip && header.zip === "true";
|
|
|
|
const { decryptKey } = this.state;
|
|
const plain = dec(cipher, decryptKey, zip);
|
|
const expires = !!header.exp ? (!!header.burn ? "Burn on Read" : ((d) => d.toLocaleDateString() + " " + d.toLocaleTimeString())(new Date(header.exp*1000))) : "Never";
|
|
this.setState({cipher: tx, plain: plain, expires, syntax: header.lang});
|
|
}
|
|
|
|
encrypt(input) {
|
|
const rnd = rng(40);
|
|
const decryptKey = sha(rnd);
|
|
|
|
const gzip = blength(input)>4000;
|
|
const plain = gzip?zip(input):input;
|
|
|
|
const { syntax, expire, burn } = this.state;
|
|
|
|
const header = {
|
|
chk: chk(rnd),
|
|
lang: syntax,
|
|
exp: parseInt(expire, 10) + (Date.now() / 1000 | 0),
|
|
zip: gzip,
|
|
burn: burn
|
|
}
|
|
|
|
let s = '', e = enc(plain, decryptKey);
|
|
while (e.length > 79) {
|
|
s += e.slice(0, 79) + "\n";
|
|
e = e.slice(79);
|
|
}
|
|
s += e + "\n";
|
|
|
|
const expires = !!header.exp ? (!!header.burn ? "Burn on Read" : ((d) => d.toLocaleDateString() + " " + d.toLocaleTimeString())(new Date(header.exp*1000))) : "Never";
|
|
const cipher = Object.entries(header).map(([k, v]) => !!v ? k + ":\t" + v + "\n" : "").join('') + "\n" + s;
|
|
|
|
return req(PASTE_API).post({}, cipher)
|
|
.then((r) => r.text())
|
|
.then((d) => {
|
|
console.log("Received:\n" + d);
|
|
const [ok='', hash=''] = d.split(' ', 2);
|
|
if (ok === "OK")
|
|
this.setState((state) => ({cipher, gzip, decryptKey, hash, expires}));
|
|
else console.log(d);
|
|
return [hash, decryptKey];
|
|
});
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.startEntropy(['mousemove', 'keydown', 'keypress', 'click', 'scroll'], 16);
|
|
req(`${PASTE_API}/rng`).get().then((res)=>res.text()).then(this.addEntropy).catch();
|
|
|
|
const { hash } = this.state;
|
|
if (hash !== "")
|
|
req(`${PASTE_API}/${hash}`).get().then((res)=> res.ok ? res.text() : "").then(this.decrypt).catch();
|
|
}
|
|
componentWillReceiveProps(nextProps) {
|
|
const { hash:nextHash } = window.location;
|
|
const [ hash='', key='' ] = nextHash.substring(2).split("!");
|
|
|
|
if (hash === this.state.hash) return;
|
|
|
|
if (hash === '') this.setState({cipher:"", plain:"", hash: "", decryptKey: "", syntax: "text", expire: 604800, burn: false})
|
|
else {
|
|
this.setState({hash: hash, decryptKey: key});
|
|
req(`${PASTE_API}/${hash}`).get().then((res)=> res.ok ? res.text() : "").then(this.decrypt).catch();
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const { hash } = this.state;
|
|
return hash === '' ? <PasteCreate {...this.state} onChange={this.onChange} onSubmit={this.onSubmit}/> : <PasteView {...this.state} onNew={this.onNew} onCopy={this.onCopy}/> ;
|
|
}
|
|
}
|
|
|
|
|
|
function PasteCreate({error, onSubmit, onChange, syntax, syntaxItems, expire, expireItems, burn, entropy, plain}) {
|
|
return (
|
|
<section className="container">
|
|
<div>
|
|
{!!error &&
|
|
<Alert warning>
|
|
<strong>Holy guacamole!</strong> {error}
|
|
</Alert>}
|
|
|
|
<Form name='paste' onSubmit={onSubmit}>
|
|
<div className="form-inline">
|
|
<label className="my-1 mr-2" for="syntax">Syntax</label>
|
|
<select className='custom-select my-1 mr-sm-2' id="syntax" name="syntax" onChange={onChange} value={syntax}>
|
|
{syntaxItems.map((o) => <option key={o[0]} value={o[0]}>{o[1]}</option>)}
|
|
</select>
|
|
|
|
<label className="my-1 mr-2" for="expire">Expires</label>
|
|
<select className='custom-select my-1 mr-sm-2' id="expire" name="expire" onChange={onChange} value={expire}>
|
|
{expireItems.map((o) => <option key={o[0]} value={o[0]}>{o[1]}</option>)}
|
|
</select>
|
|
|
|
<div className="custom-control custom-checkbox my-1 mr-sm-2">
|
|
<input className="custom-control-input" type='checkbox' id="burn-on-read" name="burn" onChange={onChange} value={burn}/>
|
|
<label className="custom-control-label" for="burn-on-read">Burn on Read</label>
|
|
</div>
|
|
</div>
|
|
|
|
<textarea required className='form-control' rows='20' name="plain" onChange={onChange} value={plain}></textarea>
|
|
<pre className="grey">Additional Entropy: {entropy} bytes / Content size: {blength(plain)} bytes</pre>
|
|
|
|
<Button type='submit' className='btn btn-default btn-lg btn-block'>Encrypt</Button>
|
|
</Form>
|
|
<br/>
|
|
|
|
<div className="card">
|
|
<div className="card-body">
|
|
<p>Create pastes from the command line! <a href={`${window.location.origin}/ui/paste.sh`} download>paste.sh</a></p>
|
|
<pre>{`$ echo /etc/passwd | ./paste.sh
|
|
|
|
env options:
|
|
PASTE_URL - Set the url base for paste operations (default: HTTPS://paste.dn42.us)
|
|
PASTE_GZIP - 0 = No Compression, 1 = Use gzip compression (default: 0)
|
|
PASTE_BURN - 0 = No Burn on Read, 1 = Burn on read (default: 0)
|
|
PASTE_DATE - Value to be used when setting expire date. (default: next-week)`}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function PasteView({hash, decryptKey, expires, burn, gzip, cipher, plain, syntax, onNew, onCopy}) {
|
|
const gzipOpts = gzip ? '| gzip -dc' : '';
|
|
return (
|
|
<section className="container">
|
|
<div className="input-group">
|
|
<span className="input-group-prepend">
|
|
<button className="btn btn-default" type="button" onClick={onNew}>New</button>
|
|
</span>
|
|
<input type='text' readOnly className='form-control' value={`${window.location.origin}/ui/#/${hash}!${decryptKey}`} onClick={(e) => e.target.select()}/>
|
|
<span className="input-group-append">
|
|
<button className="btn btn-default" type="button" onClick={onCopy}>Copy</button>
|
|
</span>
|
|
</div>
|
|
|
|
<br/>
|
|
|
|
<div className="card">
|
|
<div className='card-header'>
|
|
<b>Lang:</b> {syntax}
|
|
|
|
<b>Expires:</b> {expires}
|
|
|
|
{burn && (<b> BURN ON READ </b>)}
|
|
</div>
|
|
<div className="card-body">
|
|
{syntax==="markdown" ? (
|
|
<Markdown source={plain} />
|
|
) : (
|
|
<Highlight className={syntax}>{plain}</Highlight>
|
|
)}
|
|
</div>
|
|
|
|
<div className="card-footer">
|
|
<pre>{`# Command Line:
|
|
curl -s "${PASTE_API}/${hash}" \\
|
|
| sed "1,/^\\$/d" \\
|
|
| openssl aes-256-cbc -md md5 \\
|
|
-d -a -k "${decryptKey}" ${gzipOpts}
|
|
|
|
${cipher}`}</pre>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export default Paste;
|