diff --git a/lib/bitauth.js b/lib/bitauth.js index 3a6a886..536d8e1 100644 --- a/lib/bitauth.js +++ b/lib/bitauth.js @@ -1,76 +1,172 @@ -var bitcore = require('bitcore'); -var Key = bitcore.Key; -var SIN = bitcore.SIN; -var SINKey = bitcore.SINKey -var util = bitcore.util; - -var BitAuth = {}; +var elliptic = require('elliptic'); +var ecdsa = new elliptic.ec(elliptic.curves.secp256k1); +var hashjs = require('hash.js'); +var bs58 = require('bs58'); +var BitAuth = {}; +/** + * Will return a key pair and identity + * + * @returns {Object} An object with keys: created, priv, pub and sin + */ BitAuth.generateSin = function() { - var sk = new SINKey(); - sk.generate(); - return sk.storeObj(); + + var keys = ecdsa.genKeyPair(); + + var privateKey = keys.getPrivate('hex'); + var publicKey = this.getPublicKeyFromPrivateKey(privateKey); + var sin = this.getSinFromPublicKey(publicKey); + + var sinObj = { + created: Math.round(Date.now() / 1000), + priv: privateKey, + pub: publicKey, + sin: sin + }; + + return sinObj; }; +/** + * Will return a public key from a private key + * + * @param {String} A private key in hex + * @returns {String} A compressed public key in hex + */ BitAuth.getPublicKeyFromPrivateKey = function(privkey) { - try { - var key = new Key(); - key.private = new Buffer(privkey, 'hex'); - key.regenerateSync(); + var keys = ecdsa.keyPair(privkey, 'hex'); - return key.public.toString('hex'); - } catch (err) { - console.log(err); - return null; + // compressed public key + var pubKey = keys.getPublic(); + var xbuf = new Buffer(pubKey.x.toString('hex', 64), 'hex'); + var ybuf = new Buffer(pubKey.y.toString('hex', 64), 'hex'); + var pub; + + if (ybuf[ybuf.length-1] % 2) { //odd + pub = Buffer.concat([new Buffer([3]), xbuf]); + } else { //even + pub = Buffer.concat([new Buffer([2]), xbuf]); } + + var hexPubKey = pub.toString('hex'); + + return hexPubKey; + }; +/** + * Will return a SIN from a compressed public key + * + * @param {String} A public key in hex + * @returns {String} A SIN identity + */ BitAuth.getSinFromPublicKey = function(pubkey) { - var pubkeyHash = util.sha256ripe160(new Buffer(pubkey, 'hex')); - var sin = new SIN(SIN.SIN_EPHEM, pubkeyHash); - return sin.toString(); -} + // sha256 hash the pubkey + var pubHash = (new hashjs.sha256()).update(pubkey, 'hex').digest('hex'); + + // get the ripemd160 hash of the pubkey + var pubRipe = (new hashjs.ripemd160()).update(pubHash, 'hex').digest('hex'); + + // add the version + var pubPrefixed = '0f02'+pubRipe; + + // two rounds of hashing to generate the checksum + var hash1 = (new hashjs.sha256()).update(pubPrefixed, 'hex').digest('hex'); + var checksumTotal = (new hashjs.sha256()).update(hash1, 'hex').digest('hex'); + + // slice the hash to arrive at the checksum + var checksum = checksumTotal.slice(0,8); + + // add the checksum to the ripemd160 pubkey + var pubWithChecksum = pubPrefixed + checksum; + + // encode into base58 + var sin = bs58.encode(new Buffer(pubWithChecksum, 'hex')); + + return sin; + +}; + +/** + * Will sign a string of data with a private key + * + * @param {String} data - A string of data to be signed + * @param {String} privkey - A private key in hex + * @returns {String} signature - A DER signature in hex + */ 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; - } + var hash = (new hashjs.sha256()).update(data).digest('hex'); + var signature = ecdsa.sign(hash, privkey); + var hexsignature = signature.toDER('hex'); + return hexsignature; }; -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); - } +/** + * Will verify a signature + * + * @param {String} data - A string of data that has been signed + * @param {String} pubkey - The compressed public key in hex that has signed the data + * @param {String} hexsignature - A DER signature in hex + * @returns {Function|Boolean} - If the signature is valid + */ +BitAuth.verifySignature = function(data, pubkey, hexsignature, callback) { + var hash = (new hashjs.sha256()).update(data).digest('hex'); + var signature = new Buffer(hexsignature, 'hex'); + var valid = ecdsa.verify(hash, signature, pubkey); + if (callback) + return callback(null, valid); + return valid; }; + +/** + * Will verify that a SIN is valid + * + * @param {String} sin - A SIN identity + * @returns {Function|Boolean} - If the SIN identity is valid + */ BitAuth.validateSin = function(sin, callback) { - var s = new SIN(sin); + var pubWithChecksum; + + // check for non-base58 characters try { - s.validate() + pubWithChecksum = new Buffer(bs58.decode(sin), 'hex').toString('hex'); } catch(err) { - if ( callback ) - callback(err); + if (callback) + return callback(err); return false; } - if ( callback ) - callback(null); - return true; + + // check the version + if (pubWithChecksum.slice(0, 4) !== '0f02') { + if (callback) + return callback(new Error('Invalid prefix or SIN version')); + return false; + } + + // get the checksum + var checksum = pubWithChecksum.slice(pubWithChecksum.length-8, + pubWithChecksum.length); + var pubPrefixed = pubWithChecksum.slice(0, pubWithChecksum.length-8); + + // two rounds of hashing to generate the checksum + var hash1 = (new hashjs.sha256()).update(pubPrefixed, 'hex').digest('hex'); + var checksumTotal = (new hashjs.sha256()).update(hash1, 'hex').digest('hex'); + + // check the checksum + if (checksumTotal.slice(0,8) === checksum) { + if (callback) + return callback(null); + return true; + } else { + if (callback) + return callback(new Error('Checksum does not match')); + return false; + } + }; module.exports = BitAuth; diff --git a/package.json b/package.json index c91e004..754e0c5 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,3 @@ - { "name": "bitauth", "description": "Passwordless authentication using Bitcoin cryptography", @@ -18,17 +17,23 @@ { "name": "Gordon Hall", "email": "gordon@bitpay.com" + }, + { + "name": "Braydon Fuller", + "email": "braydon@bitpay.com" } ], "scripts": { "make-dist": "sh scripts/make-dist.sh", - "test": "mocha test/*.js --reporter spec", - "postinstall": "npm run make-dist" + "postinstall": "npm run make-dist", + "test": "mocha test/*.js --reporter spec" }, "main": "index.js", "version": "0.1.1", "dependencies": { - "bitcore": "0.1.32", + "elliptic": "^0.15.12", + "hash.js": "^0.3.2", + "bs58": "^2.0.0", "request": "^2.36.0", "express": "^4.3.1", "base58-native": "^0.1.4", diff --git a/scripts/make-dist.sh b/scripts/make-dist.sh index 6b01c4b..ddc6545 100644 --- a/scripts/make-dist.sh +++ b/scripts/make-dist.sh @@ -1,10 +1,5 @@ -cd node_modules/bitcore -echo "Building browser bundle for bitcore..." -node browser/build -s lib/Key,lib/SINKey,lib/SIN,util/util -cd ../../ -cp node_modules/bitcore/browser/bundle.js dist/bitcore.bundle.js echo "Building browser bundle for bitauth..." -node_modules/.bin/browserify lib/bitauth.js -s bitauth -x buffertools -x bitcore -o dist/bitauth.bundle.js -echo "Minifying bitcore and bitauth..." -node_modules/.bin/uglifyjs dist/bitcore.bundle.js dist/bitauth.bundle.js -o dist/bitauth.browser.min.js +node_modules/.bin/browserify lib/bitauth.js -s bitauth -o dist/bitauth.bundle.js +echo "Minifying bitauth..." +node_modules/.bin/uglifyjs dist/bitauth.bundle.js --compress --mangle -o dist/bitauth.browser.min.js echo "Done!" diff --git a/test/test.bitauth.js b/test/test.bitauth.js index 67bd572..037c228 100644 --- a/test/test.bitauth.js +++ b/test/test.bitauth.js @@ -12,12 +12,31 @@ describe('bitauth', function() { var should = chai.should(); + // previously known keys for comparison + var keysKnown = { + priv: '97811b691dd7ebaeb67977d158e1da2c4d3eaa4ee4e2555150628acade6b344c', + pub: '02326209e52f6f17e987ec27c56a1321acf3d68088b8fb634f232f12ccbc9a4575', + sin: 'Tf3yr5tYvccKNVrE26BrPs6LWZRh8woHwjR' + } + + // a private key that will produce a public key with a leading zero + var privateKeyToZero = 'c6b7f6bfe5bb19b1e390e55ed4ba5df8af6068d0eb89379a33f9c19aacf6c08c'; + + // keys generated var keys = null; - var sin = 'Tf1Jc1xSbqasm5QLwwSQc5umddx2h7mAMHX'; - var sinb = 'Tf1Jc1xSbqasm5QLwwSQc5umddx2h7mAMhX'; + + // invalid checksum + var sinbad = 'Tf1Jc1xSbqasm5QLwwSQc5umddx2h7mAMhX'; + + // valid sin + var singood = 'TfG4ScDgysrSpodWD4Re5UtXmcLbY5CiUHA'; + + // data to sign var contract = 'keyboard cat'; var secret = 'o hai, nsa. how i do teh cryptos?'; var password = 's4705hiru13z!'; + + // signature from generate keys var signature = null; var enc = null; @@ -41,6 +60,11 @@ describe('bitauth', function() { done(); }); + it('should properly get compressed public key from a previously known private key', function(done) { + bitauth.getPublicKeyFromPrivateKey(keysKnown.priv).should.equal(keysKnown.pub); + done(); + }); + }); describe('#getSinFromPublicKey', function() { @@ -50,6 +74,11 @@ describe('bitauth', function() { done(); }); + it('should properly get the sin from a previously known compressed public key', function(done) { + bitauth.getSinFromPublicKey(keysKnown.pub).should.equal(keysKnown.sin); + done(); + }); + }); describe('#sign', function() { @@ -65,7 +94,30 @@ describe('bitauth', function() { describe('#verifySignature', function() { it('should verify the signature', function(done) { - bitauth.verifySignature(contract, keys.pub, signature, done); + bitauth.verifySignature(contract, keys.pub, signature, function(err, valid){ + should.not.exist(err); + should.exist(valid); + valid.should.equal(true); + done(); + }); + }); + + it('should verify the signature with leading zero public key', function(done) { + + var leadingZeroKeys = { + priv: privateKeyToZero, + pub: bitauth.getPublicKeyFromPrivateKey(privateKeyToZero) + }; + + signature = bitauth.sign(contract, leadingZeroKeys.priv); + bitauth.verifySignature(contract, leadingZeroKeys.pub, signature, function(err, valid){ + should.not.exist(err); + should.exist(valid); + valid.should.equal(true); + }); + + done(); + }); }); @@ -73,7 +125,7 @@ describe('bitauth', function() { describe('#validateSinTrue', function() { it('should validate the sin as true', function(done) { - var valid = bitauth.validateSin(sin); + var valid = bitauth.validateSin(singood); should.equal(true, valid); done(); }); @@ -82,8 +134,14 @@ describe('bitauth', function() { describe('#validateSinFalse', function() { - it('should validate the sin as false', function(done) { - var valid = bitauth.validateSin(sinb); + it('should validate the sin as false because of bad checksum', function(done) { + var valid = bitauth.validateSin(sinbad); + should.equal(false, valid); + done(); + }); + + it('should validate the sin as false because of non-base58', function(done) { + var valid = bitauth.validateSin('not#base!58'); should.equal(false, valid); done(); }); @@ -93,8 +151,9 @@ describe('bitauth', function() { describe('#validateSinCallback', function() { it('should receive error callback', function(done) { - var valid = bitauth.validateSin(sinb, function(err){ + var valid = bitauth.validateSin(sinbad, function(err){ should.exist(err); + err.message.should.equal('Checksum does not match'); done(); }); });