diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..476b7d8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +*Dockerfile* +*docker-compose* +node_modules diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..58065a5 --- /dev/null +++ b/.env.dev @@ -0,0 +1,15 @@ +#Server vars +PGP_PASSPHRASE=MY_VERY_COMPLEX_PGP_PASPHRASE +HMAC_KEY=MY_VERY_COMPLEX_HMAC_KEY +SMTP_HOST=smtp +SMTP_PORT=25 +DOMAIN=tipbox.site +IDENTITY="Tipbox " +NODE_ENV=development + +# If you need to override the default https url (eg. Tor) +#ORIGIN=http://tipbox123456789.onion + +# STMP Vars +## The domain to send mail from +MAILNAME=tipbox.site diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..7810b4e --- /dev/null +++ b/.env.sample @@ -0,0 +1,15 @@ +#Server vars +PGP_PASSPHRASE=MY_VERY_COMPLEX_PGP_PASPHRASE +HMAC_KEY=MY_VERY_COMPLEX_HMAC_KEY +SMTP_HOST=smtp +SMTP_PORT=25 +DOMAIN=tipbox.site +IDENTITY="Tipbox " +NODE_ENV=production + +# If you need to override the default https url (eg. Tor) +#ORIGIN=http://tipbox123456789.onion + +# STMP Vars +## The domain to send mail from +MAILNAME=tipbox.site diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..83b06c8 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +/node_modules +frontend/src/js/*.compiled.js \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..68be052 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,19 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es6": true + }, + "extends": [ + "standard" + ], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c45a9ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +node_modules + +.env + +*.js.map +*.compiled.js + +frontend/src/css +frontend/dist +data/caddy/* +data/keys/* +data/*.cnt + +!data/caddy/.gitkeep +!data/keys/.gitkeep diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e76775f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - 13 diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..70f57dd --- /dev/null +++ b/Caddyfile @@ -0,0 +1,11 @@ +{$DOMAIN} { + root /root + gzip + proxy / server:3000 { + transparent + } + log { + except / + } +} + diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..dcb4201 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,10 @@ +Steps to use Docker/docker-compose to run your own tipbox + +1. Edit .env and update with your own variables +1. Build the image and create a new PGP key + - docker-compose build server + - docker-compose run --rm server node ./server/utils/keygen.js + - docker-compose run --rm server yarn build +1. Run all the services + - `docker-compose up -d` + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..979a165 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM node:13.1-stretch + +RUN mkdir /app +WORKDIR /app +COPY . /app +RUN yarn install + +CMD [ "yarn", "start" ] diff --git a/Gulpfile.js b/Gulpfile.js index 3772c7f..032ab29 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -1,20 +1,25 @@ var gulp = require('gulp'), + crypto = require('crypto'), + fs = require('fs'), + packageJSON = require(__dirname + '/package.json') gutil = require('gulp-util'), source = require('vinyl-source-stream'), buffer = require('vinyl-buffer'), watchify = require('watchify'), browserify = require('browserify'), - minifyCSS = require('gulp-minify-css'), - uncss = require('gulp-uncss'), + cleanCSS = require('gulp-clean-css'); mold = require('mold-source-map'), - uglify = require('gulp-uglify'), rename = require("gulp-rename"), sourcemaps = require('gulp-sourcemaps'), less = require('gulp-less'), + replace = require('gulp-replace'), path = require('path'), + sriHash = require('gulp-sri-hash'), LessPluginAutoPrefix = require('less-plugin-autoprefix'); -var filesToCopy = ['frontend/src/*.html', 'frontend/src/img/**/*', 'frontend/src/videos/**/*', 'frontend/src/fonts/**/*', 'frontend/src/js/*.compiled.js', 'frontend/src/js/*.js.map', 'frontend/src/favicon.ico']; +var version = packageJSON.version; + +var filesToCopy = ['frontend/src/index.html', 'frontend/src/img/**/*', 'frontend/src/videos/**/*', 'frontend/src/fonts/**/*', 'frontend/src/js/*.compiled.js', 'frontend/src/js/*.js.map', 'frontend/src/favicon.ico']; var TipboxAppBundler = browserify({ entries: ['./frontend/src/js/tipbox.js'], @@ -27,7 +32,9 @@ var NavigationBundler = browserify({ }); var TipboxAppBundle = function() { - return TipboxAppBundler.bundle() + return TipboxAppBundler + .ignore('sodium') + .bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('tipbox.compiled.js')) .pipe(buffer()) @@ -35,7 +42,6 @@ var TipboxAppBundle = function() { loadMaps: true })) // Add transformation tasks to the pipeline here. - .pipe(uglify()) .pipe(sourcemaps.write('./')) .pipe(gulp.dest('frontend/src/js/')) .on('end', function() { @@ -52,7 +58,6 @@ var NavigationBundle = function() { loadMaps: true })) // Add transformation tasks to the pipeline here. - .pipe(uglify()) .pipe(sourcemaps.write('./')) .pipe(gulp.dest('frontend/src/js/')) .on('end', function() { @@ -81,7 +86,7 @@ gulp.task('less', function(done) { }); gulp.task('watch', function() { - gulp.watch('frontend/src/less/*.less', ['less']); + gulp.watch('frontend/src/less/*.less', gulp.series('less')); gulp.watch('frontend/src/**/*.js', function(diff) { // avoid infinite loop with browserify changing a .js file if (diff.path.match(/navigation.js/)) { @@ -95,24 +100,76 @@ gulp.task('watch', function() { }); }); -gulp.task('copy',['compile'], function() { - gulp.src(filesToCopy, { - base: './frontend/src/' - }) - .pipe(gulp.dest('frontend/dist')); +gulp.task('sri', () => { + return gulp.src('frontend/dist/index.html') + // do not modify contents of any referenced css- and js-files after this task... + .pipe(sriHash()) + // ... manipulating html files further, is perfectly fine + .pipe(gulp.dest('frontend/dist/')); +}); + +gulp.task('version', () => { + console.log("Updating version to ", version); + return gulp.src('frontend/dist/index.html') + // do not modify contents of any referenced css- and js-files after this task... + .pipe(replace('$VERSION$', version)) + // ... manipulating html files further, is perfectly fine + .pipe(gulp.dest('frontend/dist/')); }); -gulp.task('minify-css', ['less'], function() { +gulp.task('minify-css', gulp.series('less', function() { return gulp.src('frontend/src/css/tipbox.css') - .pipe(uncss({ - html: ['./frontend/src/index.html'], - ignore: [/\.selected/, /\.active/, /\.encrypted/, /\.slideout-menu/, /\.slideout-open/, /\.slideout-panel/, /\.text-page/, /\.donation-page/, /\.transaction-page/] - })) - .pipe(minifyCSS()) + .pipe(cleanCSS()) // .pipe(rename({ extname: '.min.css' })) .pipe(gulp.dest('frontend/dist/css/')); -}); +})); + +gulp.task('default', gulp.series('watch', 'less')); +gulp.task('compile', gulp.series('browserify-app', 'browserify-nav', 'minify-css')); + +// Output the sha256 hash of the final index.html along with the version + +gulp.task('addendum', () => { + return new Promise(function(resolve, reject) { + var hash = null + var algorithm = 'sha256' + , shasum = crypto.createHash(algorithm) + + // Updating shasum with file content + var filename = __dirname + "/frontend/dist/index.html" + , s = fs.ReadStream(filename) + s.on('data', function (data) { + shasum.update(data) + }) + + // making digest + s.on('end', function () { + hash = shasum.digest('hex') + console.log("SHA256 for index.html@" + version + " - " + hash) + resolve() + }) + }); +}) + +gulp.task('copyKey', () => { + return new Promise(function(resolve, reject) { + var serverKeyFile = './data/keys/public.key.json' + if (process.env["SERVER_PUBLIC_KEY"]) { + serverKeyFile = process.env["SERVER_PUBLIC_KEY"] + } + fs.copyFile(serverKeyFile, 'frontend/src/js/public.key.json', (err) => { + if (err) throw reject(err) + console.log('Using ', serverKeyFile, 'as public key.'); + resolve() + }); + }) +}) + +gulp.task('copy', gulp.series('copyKey', 'compile', function() { + return gulp.src(filesToCopy, { + base: './frontend/src/' + }) + .pipe(gulp.dest('frontend/dist')); +})); -gulp.task('default', ['watch', 'less']); -gulp.task('compile', ['minify-css', 'browserify-app', 'browserify-nav']); -gulp.task('build', ['copy']); +gulp.task('build', gulp.series('copy', 'version', 'sri', 'addendum')); diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 0000000..c0c6323 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,26 @@ +Attempting to upgrade + +`docker build . -t tipbox/server` + +Run yarn install to grab the latest node modules +`docker run --rm -it -v $(pwd):/app tipbox/server yarn install` + +Run the build process +`docker run --rm -it -v $(pwd):/app tipbox/server yarn run build` + +Generate Keys +`docker run --rm -it -v $(pwd):/app tipbox/server bash -c 'PASSPHRASE=1234 IDENTITY="" node ./server/utils/keygen.js'` + +Run the server +`docker run --rm -it -p 4000:3000 -e PASSPHRASE=1234 -e HOST=tipbox.d.mdp.im -e PORT=4000 -v $(pwd):/app tipbox/server yarn start` + + +## What's not working + +- Gulp process needs to be updated + - I've stripped out anything that didn't work (uglify, uncss), so I'm sure I've broken something +- Keybase doesn't seem to play nicely with browserify(Libsodium issue) + - https://github.com/keybase/node-nacl/blob/master/CHANGELOG.md#v110-2019-02-18 + - I added 'ignore' to Gulp, but haven't tested the encryption + + diff --git a/README.md b/README.md index 7784bb6..eee1b9d 100644 --- a/README.md +++ b/README.md @@ -10,53 +10,54 @@ Try it at https://tipbox.is - Nothing to install for the tipsters, they just need to open the unique URL generated at the creation of the Tipbox. - Support for photo/document upload. - Stateless, no logs in production (no information is ever saved). -- Unique information about the Tipbox is in the hash of the URL so that no one can tell who opened a particular Tipbox by monitoring the network traffic. -- PGP encryption between the frontend and the backend so that men-in-the-middle can't read the content of the requests sent to the backend. - Support for End-To-End encryption with PGP. -- Automatically fetches the PGP key of the recipient from public key servers at the creation of the tipbox if one exists (you need to manually verify and select it to avoid spoofing). +- PGP encryption between the frontend and the backend so reduce the risk of leaking information to passive man-in-the-middle. +- Automatically fetches the PGP key of the recipient from public key server (fingerprint is included in the URL to prevent alteration). ## Disclaimer *This is open source software, use at your own risk.* -There is always a tradeoff between ease of use and security (that's why you don't live in a bunker). By not requiring your potential sources to install an app, there is a risk that a hacker could tamper with the files served to them to include a key logger. Depending on your threat model, this may or may not matter. It’s all about finding the appropriate tool for the job. +There is always a tradeoff between ease of use and security. By not requiring your potential sources to install an app, there is a greater risk of exposure. Depending on your threat model, this may or may not matter. It’s all about finding the appropriate tool for the job. [Read more about the security of Tipbox](https://tipbox.is#security). -## How install +## Installation and development -### Setting up the keys for testing +### Setup for production - PASSPHRASE=1234 IDENTITY="" node ./server/utils/keygen.js - # Will generate private and public keys under the 'keys' directory - -### Running the server with the keys - - PGP_PASSPHRASE=1234 npm start +We use Docker and docker-compose to run run the entire stack (HTTPS with certs from LetsEncrypt, Express/Node Server, SMTP Server) with a minimal setup +1. Start by altering .env with your settings + - You'll need to pick a passphrase for your local PGP key and an HMAC key for validating the URLs +1. Build the image and create a new PGP key + - `docker-compose build server` + - Run the keygen script to create your keys, `docker-compose run --rm server node ./server/utils/keygen.js`. This will be saved in `/data/keys` + - `docker-compose run --rm server yarn build` +1. Run the server. Ensure you have the proper DNS records pointing domain you selected in .env to the server you're running this on. Caddy will automatically generated and confirm the domain SSL certificate from LetsEncrypt using this DNS record. + `docker-compose up -d` ### Locally for development: After cloning this repo, simply run - npm install --dev - npm run dev + docker-compose -f docker-compose.dev.yml build + docker-compose -f docker-compose.dev.yml node ./server/utils/keygen.js + docker-compose -f docker-compose.dev.yml up + +Visit http://localhost:3000 in order to view the development version of the site. This will serve the static files from `frontend/src` and watch for any change. When any file in `frontend/src/less/` or `frontend/src/js/*` changes, `gulp` will run the `less` and the `browserify` tasks. +# Validating the version of Tipbox being served -### In production mode - - npm install; - npm run build; - NODE_ENV=tipbox.is npm start; - -This will serve the static files from `frontend/dist`. - -## Generating an invitation URL -During the private beta, an invitation code is required to create a tipbox. -You can generate one with the following command line: +In order to validate that the version of Tipbox has not been altered, it's possible to compare the sha256 hash of index.html with the one checked into this repository. All external script and style assets utilize [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) hashes, meaning that any changes to the underlying Javascript or CSS will result in a different cryptographic hash for the page. - NODE_ENV=production PGP_PASSPHRASE=1234 HMAC_KEY=[HMAC_KEY of the server] node invite.js [email address] +1. Find the version of the page being served. It will be listed in the footer, at the very bottom of the page. +1. Get the hash of the current page being served. For example, if you want to check the version on https://tipbox.is, run the following +`curl https://tipbox.is | openssl dgst -sha256` +1. Compare this hash to the hash listed in VERSION.md available in this repository. +1. Alternatively, you can also compute the hash yourself with +`curl https://raw.githubusercontent.com/xdamman/tipbox/vVERSION/frontend/dist/index.html | openssl dgst -sha256` where VERSION is the version of the page your checking against. ## Frontend tests with Nightwatch diff --git a/VERSION.md b/VERSION.md new file mode 100644 index 0000000..b079241 --- /dev/null +++ b/VERSION.md @@ -0,0 +1,16 @@ +# v2.0.0-beta5 + +- Moved entire stack to Docker in order to make it easier to build and run the project +- Updated all the NPM modules, moved to Yarn to help provide a lockfile for future-proofing future builds. +- Move from SKS to keys.openpgp.org. SKS servers suffered an attack which left many user with a large number of fake keys. It's also fairly unreliable or slow. +- Reproducible and verifiable builds + - Subresource integrety on all style and script assets + - SHA256 of frontend/dist/index.html listed in VERSION.md for comparison and auditing + - Users can build with Docker and get the same hash if they have the server public key +- Removed any external scripts, and ability to load anything inline or external to the current domain via CSP +- ESLint everything +- Minor bugfixes and website updates + +### Hashes: + - tipbox.is: b21747edac9acc2fb7daaf414cf0c3dc1b5645fac73fdfa0852d95032942ac96 + diff --git a/build_shas.sh b/build_shas.sh new file mode 100755 index 0000000..b516d02 --- /dev/null +++ b/build_shas.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +SERVER_PUBLIC_KEY=./public_keys/tipbox.is.publickey.json yarn build +echo "Tipbox.is shasum - $(shasum -a 256 frontend/dist/index.html)" diff --git a/data/caddy/.gitkeep b/data/caddy/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/keys/.gitkeep b/data/keys/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..d4d60ec --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,19 @@ +version: '3' +services: + smtp: + image: namshi/smtp + env_file: + - .env.dev + + server: + build: . + env_file: + - .env.dev + depends_on: + - smtp + volumes: + - ".:/app" + ports: + - "3000:3000" + command: yarn run dev + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6d62b61 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3' +services: + smtp: + image: namshi/smtp + env_file: + - .env + logging: + driver: none # SMTP docker image is very verbose, prevent all logging + + server: + build: . + env_file: + - .env + depends_on: + - smtp + volumes: + - "./data:/app/data" + - "./frontend/dist:/app/frontend/dist" + expose: + - "3000" + + caddy: + image: abiosoft/caddy:no-stats + ports: + - "443:443" + - "80:80" + depends_on: + - server + env_file: + - .env + volumes: + - "./Caddyfile:/etc/Caddyfile" + - "./data/caddy:/root/.caddy" + diff --git a/frontend/src/img/padlock.svg b/frontend/src/img/padlock.svg new file mode 100644 index 0000000..f72af81 --- /dev/null +++ b/frontend/src/img/padlock.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/index.html b/frontend/src/index.html index 90f8316..3dc8388 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -1,6 +1,7 @@ + Tipbox @@ -57,7 +58,7 @@

Create a Tipbox

-

To enable end-to-end PGP encryption, make sure that the public PGP key associated to your email address has been published to a public key server.

+

To enable end-to-end PGP encryption, make sure that the public PGP key associated to your email address has been published to a public key server.

+ +

Potential risks

+

Any intermediary between the end user and this server (such as your ISP) could record the IP addresses that are connecting to this server. To be clear, that would only reveal who accessed this server, not the actual Tipbox, recipient or content of the tip.

+

Sophisticated attackers who could monitor the requests coming in and out of our server to do statistical analysis of the traffic based on the size of the requests. To mitigate this, we add padding to the requests sent to our server which won't reflect its real size. We also add a random delay between the request to our server and sending the tip to the recipient so that observers can't easily synchronize logs.

+

Another potential security risk would be someone hacking into our server and changing the html/javascript code being served to our users. To mitigate this, we invite you to make sure that the html page and javascript file served by this server are the same than what is available on our public repo.

+

That being said, while Tipbox is taking steps to keep your tips secure and anonymous, no security is perfect, and you should use this service at your own risk.

+

If you do require a higher degree of security, we would refer you to SecureDrop, or the Signal messaging app, available on iOS or Android.

@@ -362,111 +370,9 @@

Privacy Notice

- -
-
-
-
- $0 - - -
-
-
-
-
-
-
- -
- - - -
- -
-
-
-
- $0 - - -
-
-
-
-
-
- -
-

Latest Activities

-
    -
    -