├── index.js ├── .gitignore ├── .travis.yml ├── test ├── eat_test.js ├── encode_test.js └── decode_test.js ├── Gruntfile.js ├── package.json ├── lib ├── eat.js ├── decode.js └── encode.js ├── LICENSE └── README.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/eat'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.sw? 2 | node_modules/ 3 | **/*.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | -------------------------------------------------------------------------------- /test/eat_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var eat = require('../index.js'); 5 | 6 | describe('csrft object', function() { 7 | it('should be able an object', function() { 8 | expect(typeof(eat) === 'object'); 9 | }); 10 | 11 | it('should be able to generate salt async', function(done) { 12 | eat.genSalt(function() { 13 | expect(typeof(eat.salt)).to.eql('object'); 14 | done(); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | grunt.loadNpmTasks('grunt-simple-mocha'); 5 | grunt.loadNpmTasks('grunt-contrib-jshint'); 6 | var files = ['lib/**/*.js', 'test/**/*test.js', 'Gruntfile.js']; 7 | 8 | grunt.initConfig({ 9 | jshint: { 10 | options: { 11 | node: true, 12 | globals: { 13 | describe: false, 14 | it: false, 15 | before: false 16 | } 17 | }, 18 | src: files 19 | } 20 | }); 21 | 22 | grunt.registerTask('default', ['jshint']); 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eat", 3 | "version": "0.2.1", 4 | "description": "Encrypted Authentication Tokens", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node_modules/mocha/bin/mocha test/*.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/toastynerd/eat" 12 | }, 13 | "licences": { 14 | "type": "MIT", 15 | "url": "https://raw.github.com/toastynerd/eat/master/LICENSE" 16 | }, 17 | "author": "Tyler Morgan ", 18 | "homepage": "https://github.com/toastynerd/eat", 19 | "bugs": { 20 | "url": "https://github.com/toastynerd/eat/issues" 21 | }, 22 | "devDependencies": { 23 | "chai": "^1.10.0", 24 | "grunt": "^0.4.5", 25 | "grunt-contrib-jshint": "^0.10.0", 26 | "grunt-simple-mocha": "^0.4.0", 27 | "mocha": "^2.1.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/encode_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var eat = require('../index'); 5 | 6 | describe('encode a token', function() { 7 | before(function(done) { 8 | //this is actually done on iniation but we're doing it a second time 9 | eat.genSalt(function(){ 10 | done(); 11 | }); 12 | }); 13 | 14 | it('encode should return a string', function(done) { 15 | eat.encode({hello: "world"}, 'sometestsecret', function(err, token) { 16 | expect(err).to.eql(null); 17 | expect(typeof(token)).to.eql('string'); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should not encode invaild JSON', function(done) { 23 | eat.encode("{hello: broken", 'sometestsecret', function(err, token) { 24 | expect(err).to.not.eql(null); 25 | expect(token).to.eql(undefined); 26 | expect(err.message).to.eql('invalid JSON'); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /lib/eat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'); 4 | var encode = require('./encode'); 5 | var decode = require('./decode'); 6 | 7 | var Eat = function(options) { 8 | options = options || {}; 9 | this.saltSize = options.saltSize || 32; 10 | this.hashIterations = options.hashIterations || 16; 11 | this.salt = crypto.randomBytes(this.saltSize); 12 | this.iv = crypto.randomBytes(16); 13 | this.digest = options.digest || 'sha512'; 14 | }; 15 | 16 | Eat.prototype.genSalt = function(callback) { 17 | crypto.randomBytes(this.saltSize, function(err, buf) { 18 | this.salt = buf; 19 | callback(); 20 | }.bind(this)); 21 | }; 22 | 23 | Eat.prototype.geniv = function(callback) { 24 | crypto.randomBytes(32, function(err, buf) { 25 | this.iv = buf; 26 | callback(); 27 | }.bind(this)); 28 | }; 29 | 30 | Eat.prototype.encode = encode; 31 | Eat.prototype.decode = decode; 32 | 33 | var eat = new Eat(); 34 | 35 | module.exports = eat; 36 | -------------------------------------------------------------------------------- /test/decode_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var eat = require('../index'); 5 | 6 | describe('encode and decode', function() { 7 | var token; 8 | before(function(done) { 9 | eat.encode({hello: 'world'}, 'testpassword', function(err, encrypted) { 10 | token = encrypted; 11 | done(); 12 | }); 13 | }); 14 | 15 | it('should be able to decode with the same key', function(done) { 16 | eat.decode(token, 'testpassword', function(err, decoded) { 17 | expect(err).to.eql(null); 18 | expect(decoded.hello).to.eql('world'); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should not be able to decode with an invalid key', function(done) { 24 | eat.decode(token, 'wrongpassword', function(err, decoded) { 25 | expect(err).to.not.eql(null); 26 | expect(decoded).to.eql(undefined); 27 | expect(err.message).to.eql('could not decode token'); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) <2015> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/decode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto'); 4 | var inherits = require('util').inherits; 5 | var EventEmitter = require('events').EventEmitter; 6 | 7 | var Decode = function() { 8 | EventEmitter.call(this); 9 | }; 10 | 11 | inherits(Decode, EventEmitter); 12 | 13 | module.exports = function(token, appKey, callback) { 14 | var decode = new Decode(); 15 | 16 | decode.on('done', function(decoded) { 17 | var decodedJSON; 18 | try { 19 | decodedJSON = JSON.parse(decoded); 20 | } catch(e) { 21 | return callback(new Error('could not decode token')); 22 | } 23 | callback(null, decodedJSON); 24 | }); 25 | 26 | decode.on('derivedKeyDone', function(key) { 27 | var decipher = crypto.createDecipheriv('aes-256-ctr', key, this.iv); 28 | var decoded = decipher.update(token, 'base64', 'utf8'); 29 | decoded += decipher.final('utf8'); 30 | decode.emit('done', decoded); 31 | }.bind(this)); 32 | 33 | crypto.pbkdf2(appKey, this.salt, this.hashIterations, 32, this.digest, function(err, derivedKey) { 34 | if (err) throw err; 35 | decode.emit('derivedKeyDone', derivedKey); 36 | }.bind(this)); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/encode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var crypto = require('crypto'); 3 | var domain = require('domain').create(); 4 | var inherits = require('util').inherits; 5 | var EventEmitter = require('events').EventEmitter; 6 | 7 | var Encode = function(data, key) { 8 | EventEmitter.call(this); 9 | }; 10 | 11 | inherits(Encode, EventEmitter); 12 | 13 | module.exports = function(data, appKey, callback) { 14 | var encode = new Encode(); 15 | var jsonData; 16 | 17 | if (typeof (data) == 'string') 18 | jsonData = data; 19 | else 20 | jsonData = JSON.stringify(data); 21 | 22 | //see if the string is valid json 23 | try { 24 | JSON.parse(jsonData); 25 | } catch(e) { 26 | return callback(new Error('invalid JSON')); 27 | } 28 | 29 | encode.on('done', function(token) { 30 | callback(null, token); 31 | }.bind(this)); 32 | 33 | encode.on('derivedKeyDone', function(key) { 34 | var cipher = crypto.createCipheriv('aes-256-ctr', key, this.iv); 35 | var token = cipher.update(jsonData, 'utf8', 'base64'); 36 | token += cipher.final('base64'); 37 | encode.emit('done', token); 38 | }.bind(this)); 39 | 40 | crypto.pbkdf2(appKey, this.salt, this.hashIterations, 32, this.digest, function(err, derivedKey) { 41 | if (err) throw err; 42 | encode.emit('derivedKeyDone', derivedKey); 43 | }.bind(this)); 44 | }; 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Encrypted Authentication Tokens 2 | ====================== 3 | ![travis build](https://travis-ci.org/toastynerd/eat.svg?branch=master) 4 | 5 | Tokens used for authentication purposes in a client/server app architecture. 6 | Loosely based off the [encrypted token pattern by OWASP for preventing CSRF attacks](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet). 7 | Tokens are encrypted using aes-256-ctr with a random IV and a password that's generated 8 | using crypto.pbkdf2 from an app secret with a 32 byte random salt. 9 | 10 | ```javascript 11 | var eat = require('eat'); 12 | 13 | eat.encode({id: user.id, timestamp: Time.now}, 'mysupersecret', function(err, token) { 14 | if (err) throw err; 15 | //send token 16 | }); 17 | 18 | eat.decode(token, 'mysupersecret', function(err, token) { 19 | if (err) throw err; 20 | //check if token is expired and if the id corresponds to a valid user 21 | }); 22 | ``` 23 | 24 | The resulting token will be base64 encoded and can be passed to client to use for 25 | authentication against the server. The token should only be able to be encoded on the 26 | server. *This is not a substitute for using ssl/tls* it should be used in conjunction 27 | with a secure connection. If an attacker gets ahold of the token they will be able 28 | to authenticate as the user until the token is expired or you change the salt/iv. 29 | These functions can be called on eat as follows. 30 | 31 | ```javascript 32 | if (server_compromised) { 33 | eat.genSalt(function() { 34 | console.log('salt generated'); 35 | }); 36 | 37 | eat.geniv(function() { 38 | console.log('iv generated'); 39 | }); 40 | } 41 | ``` 42 | Either one of these functions will invalidate any token generated when 43 | using a different iv/salt. Another thing to keep in mind is that the 44 | iv/salt get regenerated everytime the server is reset. So, a server 45 | reset will invalidate all current tokens which might not be ideal for 46 | your use case. You can set the iv/salt parameters of the eat object 47 | although be careful as this can open you up to replay attacks. The salt 48 | can be length but the iv MUST be 32 bytes. Both must be contained in a buffer. 49 | ```javascript 50 | var eat = require('eat'); 51 | eat.salt = buffer.new('my new salt'); 52 | eat.iv = buffer.new('a 32 byte string'); //this string isn't actually 32 bytes 53 | ``` 54 | --------------------------------------------------------------------------------