diff --git a/.gitignore b/.gitignore index da23d0d..ed4b243 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,3 @@ -# Logs -logs -*.log - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# Deployed apps should consider commenting this line out: -# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git node_modules +dist/bitauth.browser.js +dist/bitauth.browser.min.js diff --git a/README.md b/README.md index feb4a4e..574cbc3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,180 @@ -bitauth +BitAuth ======= + +Passwordless authentication using Bitcoin cryptography + +# Overview + +BitAuth is a way to do secure, passwordless authentication using the cryptography +in Bitcoin. Instead of using a shared secret, the client signs each request using +a private key and the server checks to make sure the signature is valid and matches +the public key. + +## Advantages over other authentication mechanisms + +* By signing each request, man in the middle attacks are impossible. +* A nonce is part of the data signed, which prevents replay attacks. +* The cryptography in Bitcoin is rock solid and is securing billions + of dollars worth of bitcoins. +* It uses elliptic curve cryptography which performs much better than RSA. +* Because the private key is never revealed to the server, it does +not need to be exchanged between the server and client over a side channel like +in HMAC. + +## Technical Overview +BitAuth uses the same technology in Bitcoin. A public private key pair is created +using elliptic curve secp256k1. The public SIN (System identification number), +like a bitcoin address, is the RIPEMD 160, SHA256 hash of the public key. +See https://en.bitcoin.it/wiki/Identity_protocol_v1 for complete details. + +In each request, the client includes a nonce to prevent replay attacks. The client +signs the full url with the request body concatenated if there is one. The signature +is included in the x-signature header and the public key is included in the +x-pubkey header. + +The server verifies that the signature is valid and that it matches the public key. +It then computes the SIN from the public key, and sees whether that SIN has access +to the requested resource. The nonce is checked to make sure it is higher than +the previously used nonce. + +## Technology is readily available + +With the growing popularity of Bitcoin, there are already libraries written in +many languages. Because BitAuth uses the same technology as Bitcoin, it is easy +to start using BitAuth. + + +## Problems with password authentication + +* Have to keep track of a separate password for every web service. People forget +passwords, encouraging them to reuse passwords and opening themselves up to +having multiple services compromised. +* Brute force attacks on weak passwords. +* Passwords may travel over plaintext +* Passwords in databases being leaked +* Phishing attacks to steal passwords + +## Passwordless based authentication across web services + +With BitAuth, users can use the same, strong password to encrypt their keys and +not worry about one service gaining access to another. + +In the future, an identity system could be built around BitAuth keys where a user +could create one key to represent an identity which could authenticate against +multiple services. + +In order for this to work, there would have to be a browser +integration or plugin which would manage these identities and a Javascript API +where websites could sign requests going to their website with the private key, +but without exposing the private key to the third party sites. + +There also needs to be a public place to store SIN's, preferably in +a decentralized blockchain or datastore like namecoin. Key revocations could +be stored here as well as reviews/feedback to build a reputation around an +identity. + +# Getting Started + +Example server + +``` +var express = require('express'); +var bodyParser = require('body-parser'); +var rawBody = require('../lib/middleware/rawbody'); +var bitauth = require('../lib/middleware/bitauth'); + +var users = { + 'Tf7UNQnxB8SccfoyZScQmb34V2GdEtQkzDz': {name: 'Alice'}, + 'Tf22EUFxHWh4wmA3sDuw151W5C5g32jgph2': {name: 'Bob'} +}; + +var pizzas = []; + +var app = express(); +app.use(rawBody); +app.use(bodyParser()); + + +app.get('/user', bitauth, function(req, res) { + if(!req.sin || !users[req.sin]) return res.send(401, {error: 'Unauthorized'}); + res.send(200, users[req.sin]); +}); + +app.post('/pizzas', bitauth, function(req, res) { + if(!req.sin || !users[req.sin]) return res.send(401, {error: 'Unauthorized'}); + var pizza = req.body; + pizza.owner = users[req.sin].name; + pizzas.push(pizza); + res.send(200, req.body); +}); + +app.get('/pizzas', function(req, res) { + res.send(200, pizzas); +}); + +app.listen(3000); +``` + +Example client + +``` +var request = require('request'); +var bitauth = require('../lib/bitauth'); + +// These can be generated with bitauth.generateSin() +var keys = { + alice: '38f93bdda21a5c4a7bae4eb75bb7811cbc3eb627176805c1009ff2099263c6ad', + bob: '09880c962437080d72f72c8c63a69efd65d086c9e7851a87b76373eb6ce9aab5' +}; + +// GET + +for(k in keys) { + var url = 'http://localhost:3000/user'; + var dataToSign = url; + var options = { + url: url, + headers: { + 'x-pubkey': bitauth.getPublicKeyFromPrivateKey(keys[k]), + 'x-signature': bitauth.sign(dataToSign, keys[k]) + } + }; + + request.get(options, function(err, response, body) { + if(err) { + console.log(err); + } + if(body) { + console.log(body); + } + }); +} + +var pizzas = ['pepperoni', 'sausage', 'veggie', 'hawaiian']; + +// POST + +for(k in keys) { + var url = 'http://localhost:3000/pizzas'; + var data = {type: pizzas[Math.floor(Math.random() * pizzas.length)]}; + var dataToSign = url + JSON.stringify(data); + var options = { + url: url, + headers: { + 'x-pubkey': bitauth.getPublicKeyFromPrivateKey(keys[k]), + 'x-signature': bitauth.sign(dataToSign, keys[k]) + }, + json: data + }; + + request.post(options, function(err, response, body) { + if(err) { + console.log(err); + } + if(body) { + console.log(body); + } + }); +} + +``` diff --git a/dist/README.md b/dist/README.md new file mode 100644 index 0000000..f503e49 --- /dev/null +++ b/dist/README.md @@ -0,0 +1,11 @@ +# BitAuth Browser Bundle + +To build a browser compatible version of BitAuth, run the following command from +the project's root directory: + +``` +npm run make-dist +``` + +This will output `bitauth.browser.js` to this directory. The script introduces a +global variable at `window.bitauth`. diff --git a/examples/client.js b/examples/client.js new file mode 100644 index 0000000..95bec5e --- /dev/null +++ b/examples/client.js @@ -0,0 +1,58 @@ +var request = require('request'); +var bitauth = require('../lib/bitauth'); + +// These can be generated with bitauth.generateSin() +var keys = { + alice: '38f93bdda21a5c4a7bae4eb75bb7811cbc3eb627176805c1009ff2099263c6ad', + bob: '09880c962437080d72f72c8c63a69efd65d086c9e7851a87b76373eb6ce9aab5' +}; + +// GET + +for(k in keys) { + var url = 'http://localhost:3000/user'; + var dataToSign = url; + var options = { + url: url, + headers: { + 'x-pubkey': bitauth.getPublicKeyFromPrivateKey(keys[k]), + 'x-signature': bitauth.sign(dataToSign, keys[k]) + } + }; + + request.get(options, function(err, response, body) { + if(err) { + console.log(err); + } + if(body) { + console.log(body); + } + }); +} + +var pizzas = ['pepperoni', 'sausage', 'veggie', 'hawaiian']; + +// POST + +for(k in keys) { + var url = 'http://localhost:3000/pizzas'; + var data = {type: pizzas[Math.floor(Math.random() * pizzas.length)]}; + var dataToSign = url + JSON.stringify(data); + var options = { + url: url, + headers: { + 'x-pubkey': bitauth.getPublicKeyFromPrivateKey(keys[k]), + 'x-signature': bitauth.sign(dataToSign, keys[k]) + }, + json: data + }; + + request.post(options, function(err, response, body) { + if(err) { + console.log(err); + } + if(body) { + console.log(body); + } + }); +} \ No newline at end of file diff --git a/examples/server.js b/examples/server.js new file mode 100644 index 0000000..a9e382c --- /dev/null +++ b/examples/server.js @@ -0,0 +1,35 @@ +var express = require('express'); +var bodyParser = require('body-parser'); +var rawBody = require('../lib/middleware/rawbody'); +var bitauth = require('../lib/middleware/bitauth'); + +var users = { + 'Tf7UNQnxB8SccfoyZScQmb34V2GdEtQkzDz': {name: 'Alice'}, + 'Tf22EUFxHWh4wmA3sDuw151W5C5g32jgph2': {name: 'Bob'} +}; + +var pizzas = []; + +var app = express(); +app.use(rawBody); +app.use(bodyParser()); + + +app.get('/user', bitauth, function(req, res) { + if(!req.sin || !users[req.sin]) return res.send(401, {error: 'Unauthorized'}); + res.send(200, users[req.sin]); +}); + +app.post('/pizzas', bitauth, function(req, res) { + if(!req.sin || !users[req.sin]) return res.send(401, {error: 'Unauthorized'}); + var pizza = req.body; + pizza.owner = users[req.sin].name; + pizzas.push(pizza); + res.send(200, req.body); +}); + +app.get('/pizzas', function(req, res) { + res.send(200, pizzas); +}); + +app.listen(3000); \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..14c9f38 --- /dev/null +++ b/index.js @@ -0,0 +1,9 @@ +// get base functionality +var bitauth = require('./lib/bitauth'); + +// add node-specific encrypt/decrypt +bitauth.encrypt = require('./lib/encrypt'); +bitauth.decrypt = require('./lib/decrypt'); + + +module.exports = bitauth; diff --git a/lib/bitauth.js b/lib/bitauth.js new file mode 100644 index 0000000..98fc875 --- /dev/null +++ b/lib/bitauth.js @@ -0,0 +1,62 @@ +var crypto = require('crypto'); +var bitcore = require('bitcore'); +var Key = bitcore.Key; +var SIN = bitcore.SIN; +var SINKey = bitcore.SINKey +var util = bitcore.util; + +var BitAuth = {}; + +BitAuth.generateSin = function() { + var sk = new SINKey(); + sk.generate(); + return sk.storeObj(); +}; + +BitAuth.getPublicKeyFromPrivateKey = function(privkey) { + try { + var key = new Key(); + + key.private = new Buffer(privkey, 'hex'); + key.regenerateSync(); + + return key.public.toString('hex'); + } catch (err) { + console.log(err); + return null; + } +}; + +BitAuth.getSinFromPublicKey = function(pubkey) { + var pubkeyHash = util.sha256ripe160(new Buffer(pubkey, 'hex')); + var sin = new SIN(SIN.SIN_EPHEM, pubkeyHash); + return sin.toString(); +} + +BitAuth.sign = function(data, privkey) { + var hash = util.sha256(data); + + try { + var key = new Key(); + key.private = new Buffer(privkey, 'hex'); + return key.signSync(hash).toString('hex'); + } catch (err) { + console.log(err.stack); + console.log(err); + return null; + } +}; + +BitAuth.verifySignature = function(data, pubkey, signature, callback) { + var hash = util.sha256(data); + + try { + var key = new Key(); + key.public = new Buffer(pubkey, 'hex'); + key.verifySignature(hash, new Buffer(signature, 'hex'), callback); + } catch (err) { + callback(err, false); + } +}; + +module.exports = BitAuth; diff --git a/lib/decrypt.js b/lib/decrypt.js new file mode 100644 index 0000000..87fe9c9 --- /dev/null +++ b/lib/decrypt.js @@ -0,0 +1,14 @@ +var base58 = require('base58-native'); +var crypto = require('crypto'); + +module.exports = function decrypt(password, str) { + var aes256 = crypto.createDecipher('aes-256-cbc', password); + var a = aes256.update(base58.decode(str)); + var b = aes256.final(); + var buf = new Buffer(a.length + b.length); + + a.copy(buf, 0); + b.copy(buf, a.length); + + return buf.toString('utf8'); +}; diff --git a/lib/encrypt.js b/lib/encrypt.js new file mode 100644 index 0000000..ec40873 --- /dev/null +++ b/lib/encrypt.js @@ -0,0 +1,14 @@ +var base58 = require('base58-native'); +var crypto = require('crypto'); + +module.exports = function encrypt(password, str) { + var aes256 = crypto.createCipher('aes-256-cbc', password); + var a = aes256.update(str, 'utf8'); + var b = aes256.final(); + var buf = new Buffer(a.length + b.length); + + a.copy(buf, 0); + b.copy(buf, a.length); + + return base58.encode(buf); +}; diff --git a/lib/middleware/bitauth.js b/lib/middleware/bitauth.js new file mode 100644 index 0000000..b15fb86 --- /dev/null +++ b/lib/middleware/bitauth.js @@ -0,0 +1,24 @@ +var bitauth = require('../bitauth'); + +module.exports = function(req, res, next) { + if(req.headers && req.headers['x-pubkey'] && req.headers['x-signature']) { + // Check signature is valid + // First construct data to check signature on + var fullUrl = req.protocol + '://' + req.get('host') + req.url; + var data = fullUrl + req.rawBody; + + bitauth.verifySignature(data, req.headers['x-pubkey'], req.headers['x-signature'], function(err, result) { + if(err || !result) { + return res.send(400, {error: 'Invalid signature'}); + } + + // Get the SIN from the public key + var sin = bitauth.getSinFromPublicKey(req.headers['x-pubkey']); + if(!sin) return res.send(400, {error: 'Bad public key'}); + req.sin = sin; + next(); + }); + } else { + next(); + } +}; \ No newline at end of file diff --git a/lib/middleware/rawbody.js b/lib/middleware/rawbody.js new file mode 100644 index 0000000..6d92be6 --- /dev/null +++ b/lib/middleware/rawbody.js @@ -0,0 +1,7 @@ +module.exports = function(req, res, next) { + req.rawBody = ''; + req.on('data', function(chunk) { + req.rawBody += chunk; + }); + next(); +}; \ No newline at end of file diff --git a/package.json b/package.json index b6f81f4..c8b3a53 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,35 @@ + { "name": "bitauth", - "version": "0.0.1", - "author": "Satoshi Nakamoto ", - "description": "The secure authentication framework, built on Bitcore.", - "contributors": [ - { - "name": "Patrick Nagurny", - "email": "patrick@bitpay.com" - } - ], - "keywords": [ - "bitcoin", - "SIN", - "System Identification Number", - "token" - ], - "dependencies" : { - "bitcore" : ">0.1.0" + "description": "Passwordless authentication using Bitcoin cryptography", + "author": { + "name": "Patrick Nagurny", + "email": "patrick@bitpay.com" }, - "license": "MIT", - "engines": { - "node": ">=0.10" + "contributors": [ + { + "name": "Gordon Hall", + "email": "gordon@bitpay.com" + } + ], + "scripts": { + "make-dist": "sh scripts/make-dist.sh", + "test": "node_modules/.bin/mocha test/* --reporter spec", + "postinstall": "npm run make-dist" + }, + "main": "index.js", + "version": "0.1.1", + "dependencies": { + "bitcore": ">= 0.1.9", + "request": "^2.36.0", + "express": "^4.3.1", + "base58-native": "^0.1.4", + "body-parser": "^1.2.0" + }, + "devDependencies": { + "uglify-js": "~2.4.14", + "browserify": "~4.1.11", + "should": "~4.0.4", + "mocha": "~1.20.1" } -} \ No newline at end of file +} diff --git a/scripts/make-dist.sh b/scripts/make-dist.sh new file mode 100644 index 0000000..4a2650e --- /dev/null +++ b/scripts/make-dist.sh @@ -0,0 +1,11 @@ +cd node_modules/bitcore +echo "Building browser bundle for bitcore..." +node browser/build -s lib/Key,lib/SINKey,lib/SIN,util/util +echo "Building browser bundle for bitauth..." +cd ../../ +node_modules/.bin/browserify lib/bitauth.js -s bitauth -x buffertools -i bitcore -o dist/bitauth.browser.js +echo "Compiling bitcore and bitauth..." +node_modules/.bin/uglifyjs node_modules/bitcore/browser/bundle.js dist/bitauth.browser.js -b -o dist/bitauth.browser.js +echo "Minifying bundle..." +node_modules/.bin/uglifyjs dist/bitauth.browser.js -o dist/bitauth.browser.min.js +echo "Done!" diff --git a/test/bitauth.js b/test/bitauth.js new file mode 100644 index 0000000..e2d2b12 --- /dev/null +++ b/test/bitauth.js @@ -0,0 +1,82 @@ +var should = require('should'); +var bitauth = require('../index'); + +describe('bitauth', function() { + + var keys = null; + var contract = 'keyboard cat'; + var secret = 'o hai, nsa. how i do teh cryptos?'; + var password = 's4705hiru13z!'; + var signature = null; + var enc = null; + + describe('#generateSin', function() { + + it('should generate a sin object', function(done) { + keys = bitauth.generateSin(); + should.exist(keys); + should.exist(keys.pub); + should.exist(keys.priv); + should.exist(keys.sin); + done(); + }); + + }); + + describe('#getPublicKeyFromPrivateKey', function() { + + it('should properly get the public key', function(done) { + bitauth.getPublicKeyFromPrivateKey(keys.priv).should.equal(keys.pub); + done(); + }); + + }); + + describe('#getSinFromPublicKey', function() { + + it('should properly get the sin', function(done) { + bitauth.getSinFromPublicKey(keys.pub).should.equal(keys.sin); + done(); + }); + + }); + + describe('#sign', function() { + + it('should sign the string', function(done) { + signature = bitauth.sign(contract, keys.priv); + should.exist(signature); + done(); + }); + + }); + + describe('#verifySignature', function() { + + it('should verify the signature', function(done) { + bitauth.verifySignature(contract, keys.pub, signature, done); + }); + + }); + + describe('#encrypt', function() { + + it('should encrypt the secret message', function(done) { + enc = bitauth.encrypt(password, secret); + should.exist(enc); + done(); + }); + + }); + + describe('#decrypt', function() { + + it('should decrypt the secret message', function(done) { + var dec = bitauth.decrypt(password, enc); + should.exist(dec); + done(); + }); + + }); + +});