commit 4009ab968474976d5b6110429e7696ba45e6ea4d Author: Patrick Nagurny Date: Tue May 27 12:04:57 2014 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..574cbc3 --- /dev/null +++ b/README.md @@ -0,0 +1,180 @@ +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/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/lib/bitauth.js b/lib/bitauth.js new file mode 100644 index 0000000..b936069 --- /dev/null +++ b/lib/bitauth.js @@ -0,0 +1,83 @@ +var base58 = require('base58-native'); +var crypto = require('crypto'); +var bitcore = require('bitcore'); +var Key = bitcore.Key; +var SIN = bitcore.SIN; +var SINKey = bitcore.SINKey +var coinUtil = bitcore.util; + +var BitAuth = function() {}; + +BitAuth.generateSin = function() { + var sk = new SINKey(); + sk.generate(); + var obj = sk.storeObj(); + + return obj; +}; + +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 = coinUtil.sha256ripe160(new Buffer(pubkey, 'hex')); + var sin = new SIN(SIN.SIN_EPHEM, pubkeyHash); + return sin.toString(); +} + +BitAuth.sign = function(data, privkey) { + var hash = coinUtil.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 = coinUtil.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); + } +}; + +BitAuth.encrypt = function(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); +}; + +BitAuth.decrypt = function(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'); +}; + +module.exports = BitAuth; 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 new file mode 100644 index 0000000..1b5ad81 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "bitauth", + "description": "Passwordless authentication using Bitcoin cryptography", + "author": { + "name": "Patrick Nagurny", + "email": "patrick@bitpay.com" + }, + "version": "0.1.0", + "dependencies": { + "bitcore": ">= 0.1.9", + "request": "^2.36.0", + "express": "^4.3.1", + "base58-native": "^0.1.4", + "body-parser": "^1.2.0" + } +}