├── .gitignore ├── .travis.yml ├── test_browser └── test.html ├── .eslintrc.json ├── example └── sessionKeysSample.js ├── index.html ├── package.json ├── CODE_OF_CONDUCT.md ├── index.js ├── test └── test.js ├── README.md └── dist └── sessionKeys.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | test_browser/test_bundle.js 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | - "11" 5 | - "10" 6 | -------------------------------------------------------------------------------- /test_browser/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "installedESLint": true, 4 | "plugins": [ 5 | "standard", 6 | "promise" 7 | ] 8 | } -------------------------------------------------------------------------------- /example/sessionKeysSample.js: -------------------------------------------------------------------------------- 1 | var sessionKeys = require('../') 2 | 3 | sessionKeys.generate('user@example.com', 'my secret password', function (err, keys) { 4 | if (err) { 5 | console.log(err.stack) 6 | } 7 | 8 | console.log(keys) 9 | }) 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

sessionKeys

4 |

Open the browser console and try:

5 | 6 | sessionKeys.generate('user@example.com', 'my secret password', function(err, keys) { console.log(keys) }) 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "session-keys", 3 | "version": "2.0.4", 4 | "description": "Derives NaCl compatible public and private encryption keys, symmetric encryption keys, and digital signature keys from an ID and password using SHA256, scrypt, and TweetNaCl.js.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test/*.js", 8 | "test-browser": "rm -f test_browser/test_bundle.js && browserify test/*.js > test_browser/test_bundle.js && open test_browser/test.html", 9 | "build": "browserify index.js --standalone sessionKeys > dist/sessionKeys.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:grempe/session-keys-js.git" 14 | }, 15 | "keywords": [ 16 | "cryptography", 17 | "public key", 18 | "private key", 19 | "keypairs", 20 | "signatures", 21 | "security", 22 | "SHA256", 23 | "NaCl", 24 | "TweetNaCl", 25 | "Curve25519", 26 | "ed25519", 27 | "scrypt", 28 | "password", 29 | "passphrase" 30 | ], 31 | "author": "Glenn Rempe (https://www.rempe.us/)", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/grempe/session-keys-js/issues" 35 | }, 36 | "homepage": "https://github.com/grempe/session-keys-js", 37 | "dependencies": { 38 | "base64-js": "^1.3.1", 39 | "fast-sha256": "^1.1.0", 40 | "scrypt-async": "^2.0.1", 41 | "tweetnacl": "^1.0.1" 42 | }, 43 | "devDependencies": { 44 | "browserify": "^16.5.0", 45 | "eslint": "^6.2.1", 46 | "eslint-config-standard": "^14.0.1", 47 | "eslint-plugin-promise": "^4.2.1", 48 | "eslint-plugin-standard": "^4.0.1", 49 | "tap-spec": "^5.0.0", 50 | "tape": "^4.11.0", 51 | "watchify": "^3.11.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at glenn@rempe.us. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ 50 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var sha256 = require('fast-sha256') 2 | var scrypt = require('scrypt-async') 3 | var nacl = require('tweetnacl') 4 | var base64 = require('base64-js') 5 | 6 | // Code inspired by: 7 | // https://github.com/kaepora/miniLock/blob/master/src/js/miniLock.js 8 | // https://github.com/jo/session25519 9 | 10 | // Extracted from tweetnacl-util-js 11 | // https://github.com/dchest/tweetnacl-util-js/blob/master/nacl-util.js#L16 12 | function decodeUTF8 (s) { 13 | var i, d, b 14 | d = unescape(encodeURIComponent(s)) 15 | b = new Uint8Array(d.length) 16 | 17 | for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i) 18 | return b 19 | } 20 | 21 | // Convert a decimal to hex with proper padding 22 | // Input: 23 | // d // A decimal number between 0-255 24 | // 25 | // Result: 26 | // Returns a padded hex string representing the decimal arg 27 | // 28 | function decToHex (d) { 29 | var hex = Number(d).toString(16) 30 | while (hex.length < 2) { 31 | hex = '0' + hex 32 | } 33 | return hex 34 | } 35 | 36 | // Convert Uint8Array of bytes to hex 37 | // Input: 38 | // arr // Uint8Array of bytes to convert to hex 39 | // 40 | // Result: 41 | // Returns a Base16 hex encoded version of arr 42 | // 43 | function byteArrayToHex (arr) { 44 | var hex, i 45 | hex = '' 46 | for (i = 0; i < arr.length; ++i) { 47 | hex += decToHex(arr[i]) 48 | } 49 | return hex 50 | } 51 | 52 | // Input: 53 | // key // User key hash (Uint8Array) 54 | // salt // Salt (username or email) (Uint8Array) 55 | // callback function 56 | // 57 | // Result: 58 | // Returns 256 bytes of scrypt derived key material in a Uint8Array, 59 | // which is then passed to the callback. 60 | // 61 | function getScryptKey (key, salt, callback) { 62 | 'use strict' 63 | 64 | scrypt(key, salt, { 65 | N: 16384, 66 | r: 8, 67 | p: 1, 68 | dkLen: 256, 69 | encoding: 'binary' 70 | }, function (derivedKey) { 71 | return callback(derivedKey) 72 | }) 73 | } 74 | 75 | // Input: 76 | // id // A UTF-8 username or email 77 | // password // A UTF-8 passphrase 78 | // callback // A callback function 79 | // 80 | // Result: 81 | // An object literal with all key material 82 | // 83 | exports.generate = function (id, password, callback) { 84 | 'use strict' 85 | 86 | var idSha256Bbytes, idSha256Hex, scryptKey, scryptSalt, byteKeys, 87 | hexKeys, naclEncryptionKeyPairs, naclEncryptionKeyPairsBase64, 88 | naclSigningKeyPairs, naclSigningKeyPairsBase64, out 89 | 90 | idSha256Bbytes = sha256(decodeUTF8(id)) 91 | idSha256Hex = byteArrayToHex(idSha256Bbytes) 92 | 93 | scryptKey = sha256(decodeUTF8(password)) 94 | scryptSalt = sha256(decodeUTF8([idSha256Hex, idSha256Hex.length, 'session_keys'].join(''))) 95 | 96 | getScryptKey(scryptKey, scryptSalt, function (scryptByteArray) { 97 | try { 98 | byteKeys = [] 99 | hexKeys = [] 100 | naclEncryptionKeyPairs = [] 101 | naclEncryptionKeyPairsBase64 = [] 102 | naclSigningKeyPairs = [] 103 | naclSigningKeyPairsBase64 = [] 104 | 105 | // Generate 8 pairs of all types of keys. The key types 106 | // at each Array index are all derived from the same key 107 | // bytes. Use different Array index values for each to ensure 108 | // they don't share common key bytes. For example: 109 | // 110 | // uuid : output.hexKeys[0] 111 | // encryption keypair : output.naclEncryptionKeyPairs[1] 112 | // signing keypair : output.naclSigningKeyPairs[2] 113 | // 114 | var b = 0 115 | for (var i = 0; i < 8; ++i) { 116 | var byteArr = scryptByteArray.subarray(b, b + 32) 117 | byteKeys.push(byteArr) 118 | hexKeys.push(byteArrayToHex(byteArr)) 119 | 120 | var naclEncryptionKeyPair = nacl.box.keyPair.fromSecretKey(byteArr) 121 | var naclSigningKeyPair = nacl.sign.keyPair.fromSeed(byteArr) 122 | 123 | naclEncryptionKeyPairs.push(naclEncryptionKeyPair) 124 | naclEncryptionKeyPairsBase64.push({ 125 | secretKey: base64.fromByteArray(naclEncryptionKeyPair.secretKey), 126 | publicKey: base64.fromByteArray(naclEncryptionKeyPair.publicKey) 127 | }) 128 | 129 | naclSigningKeyPairs.push(naclSigningKeyPair) 130 | naclSigningKeyPairsBase64.push({ 131 | secretKey: base64.fromByteArray(naclSigningKeyPair.secretKey), 132 | publicKey: base64.fromByteArray(naclSigningKeyPair.publicKey) 133 | }) 134 | 135 | b += 32 136 | } 137 | 138 | out = {} 139 | out.id = idSha256Hex 140 | out.byteKeys = byteKeys 141 | out.hexKeys = hexKeys 142 | out.naclEncryptionKeyPairs = naclEncryptionKeyPairs 143 | out.naclEncryptionKeyPairsBase64 = naclEncryptionKeyPairsBase64 144 | out.naclSigningKeyPairs = naclSigningKeyPairs 145 | out.naclSigningKeyPairsBase64 = naclSigningKeyPairsBase64 146 | 147 | return callback(null, out) 148 | } catch (err) { 149 | return callback(err) 150 | } 151 | }) 152 | } 153 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var sessionKeys = require('../') 2 | var test = require('tape') 3 | var base64 = require('base64-js') 4 | 5 | // ########################################################################## 6 | // # WARNING : WARNING : WARNING : WARNING : WARNING : WARNING : WARNING 7 | // # DO NOT CHANGE THE RESULT VALUES. THEY ARE USED BY MORE THAN ONE LIBRARY 8 | // # AND THE OUTPUTS MUST BE CONSTANT TO ENSURE INTEROPERABILITY! 9 | // # 10 | // # IF THESE TESTS NO LONGER PASS THEN INTEROPERABILITY IS BROKEN! 11 | // ########################################################################## 12 | 13 | // INTERACTIVE 14 | 15 | test('interactive test', function (t) { 16 | sessionKeys.generate('user@example.com', 'pet sprain our trial patch bg', function (err, key) { 17 | t.plan(7) 18 | 19 | t.notOk(err, 'interactive no error') 20 | 21 | t.equal(key.byteKeys.length, 8, 'interactive byteKeys length') 22 | t.equal(key.hexKeys.length, 8, 'interactive hexKeys length') 23 | t.equal(key.naclEncryptionKeyPairs.length, 8, 'interactive naclEncryptionKeyPairs length') 24 | t.equal(key.naclEncryptionKeyPairsBase64.length, 8, 'interactive naclEncryptionKeyPairsBase64 length') 25 | t.equal(key.naclSigningKeyPairs.length, 8, 'interactive naclSigningKeyPairs length') 26 | t.equal(key.naclSigningKeyPairsBase64.length, 8, 'interactive naclSigningKeyPairsBase64 length') 27 | }) 28 | }) 29 | 30 | test('interactive id test', function (t) { 31 | sessionKeys.generate('user@example.com', 'pet sprain our trial patch bg', function (err, key) { 32 | t.plan(2) 33 | 34 | t.notOk(err, 'interactive id no error') 35 | 36 | t.equals(key.id, 'b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514', 'interactive id') 37 | }) 38 | }) 39 | 40 | test('interactive byteKeys test', function (t) { 41 | sessionKeys.generate('user@example.com', 'pet sprain our trial patch bg', function (err, key) { 42 | t.plan(2) 43 | 44 | t.notOk(err, 'interactive byteKeys no error') 45 | 46 | t.deepEqual(key.byteKeys, 47 | [ 48 | new Uint8Array([227, 213, 226, 155, 22, 212, 112, 32, 60, 247, 86, 101, 125, 154, 151, 234, 71, 59, 82, 79, 8, 52, 188, 100, 91, 28, 165, 216, 183, 137, 157, 17]), 49 | new Uint8Array([1, 37, 254, 86, 67, 74, 79, 174, 191, 132, 239, 255, 168, 102, 235, 106, 29, 219, 45, 47, 237, 108, 62, 205, 151, 27, 163, 160, 14, 0, 166, 54]), 50 | new Uint8Array([81, 64, 179, 222, 199, 221, 82, 29, 93, 162, 162, 48, 44, 38, 209, 199, 21, 244, 64, 92, 47, 201, 177, 111, 93, 89, 82, 130, 203, 120, 135, 187]), 51 | new Uint8Array([128, 57, 78, 134, 230, 191, 121, 88, 50, 177, 118, 75, 63, 231, 21, 168, 106, 103, 187, 78, 254, 58, 140, 198, 237, 3, 109, 126, 68, 60, 204, 216]), 52 | new Uint8Array([125, 57, 175, 89, 168, 203, 202, 97, 200, 211, 78, 174, 118, 117, 162, 77, 206, 120, 124, 239, 76, 158, 219, 104, 27, 72, 253, 129, 100, 216, 68, 122]), 53 | new Uint8Array([244, 123, 137, 212, 254, 81, 59, 36, 159, 247, 79, 163, 24, 189, 249, 58, 104, 13, 58, 174, 84, 236, 166, 53, 158, 251, 235, 160, 188, 44, 17, 35]), 54 | new Uint8Array([70, 41, 248, 98, 4, 156, 146, 253, 236, 23, 38, 177, 1, 91, 139, 123, 15, 96, 53, 41, 168, 60, 244, 52, 89, 16, 219, 60, 29, 183, 32, 110]), 55 | new Uint8Array([61, 52, 141, 115, 90, 229, 18, 231, 253, 192, 39, 20, 196, 222, 98, 126, 178, 56, 26, 30, 100, 75, 225, 191, 81, 74, 155, 41, 78, 19, 53, 97]) 56 | ] 57 | , 'interactive byteKeys') 58 | }) 59 | }) 60 | 61 | test('interactive hexKeys test', function (t) { 62 | sessionKeys.generate('user@example.com', 'pet sprain our trial patch bg', function (err, key) { 63 | t.plan(2) 64 | 65 | t.notOk(err, 'interactive hexKeys no error') 66 | 67 | t.deepEqual(key.hexKeys, 68 | [ 69 | 'e3d5e29b16d470203cf756657d9a97ea473b524f0834bc645b1ca5d8b7899d11', 70 | '0125fe56434a4faebf84efffa866eb6a1ddb2d2fed6c3ecd971ba3a00e00a636', 71 | '5140b3dec7dd521d5da2a2302c26d1c715f4405c2fc9b16f5d595282cb7887bb', 72 | '80394e86e6bf795832b1764b3fe715a86a67bb4efe3a8cc6ed036d7e443cccd8', 73 | '7d39af59a8cbca61c8d34eae7675a24dce787cef4c9edb681b48fd8164d8447a', 74 | 'f47b89d4fe513b249ff74fa318bdf93a680d3aae54eca6359efbeba0bc2c1123', 75 | '4629f862049c92fdec1726b1015b8b7b0f603529a83cf4345910db3c1db7206e', 76 | '3d348d735ae512e7fdc02714c4de627eb2381a1e644be1bf514a9b294e133561' 77 | ] 78 | , 'interactive hexKeys') 79 | }) 80 | }) 81 | 82 | test('interactive naclEncryptionKeyPairsBase64 test', function (t) { 83 | sessionKeys.generate('user@example.com', 'pet sprain our trial patch bg', function (err, key) { 84 | t.plan(2) 85 | 86 | t.notOk(err, 'interactive naclEncryptionKeyPairsBase64 no error') 87 | 88 | t.deepEqual(key.naclEncryptionKeyPairsBase64, 89 | [ 90 | {secretKey: '49XimxbUcCA891ZlfZqX6kc7Uk8INLxkWxyl2LeJnRE=', publicKey: '9G8XJgiIXj32stQmxtUa8vmmvLGTssTrEwd9tIYpVkA='}, 91 | {secretKey: 'ASX+VkNKT66/hO//qGbrah3bLS/tbD7NlxujoA4ApjY=', publicKey: 'JH73SmhYv43j25rpC8q797XHQi4hx/DrAcQCb5i143k='}, 92 | {secretKey: 'UUCz3sfdUh1doqIwLCbRxxX0QFwvybFvXVlSgst4h7s=', publicKey: 'HmpoJqIjMnHYqvQheiCc8HXymyiGHX3ell8A+2WE330='}, 93 | {secretKey: 'gDlOhua/eVgysXZLP+cVqGpnu07+OozG7QNtfkQ8zNg=', publicKey: 'otEJhqs+cpZ1x4OHva30k4T8ye7x5eUBc2UR5IcZkhc='}, 94 | {secretKey: 'fTmvWajLymHI006udnWiTc54fO9MnttoG0j9gWTYRHo=', publicKey: 'AHHyBuknbD8/2sOe5fUWY7y9zVkOD1SrjaLdBRglVDM='}, 95 | {secretKey: '9HuJ1P5ROySf90+jGL35OmgNOq5U7KY1nvvroLwsESM=', publicKey: 'uFKZeT1VgkltnczCmljitEQswPFjQT2mS6nRKAJ4YnE='}, 96 | {secretKey: 'Rin4YgSckv3sFyaxAVuLew9gNSmoPPQ0WRDbPB23IG4=', publicKey: 'W/VA/NmgV4idPxVtzuGE8Uo5bs2fQU0L2cxhoZJFgyU='}, 97 | {secretKey: 'PTSNc1rlEuf9wCcUxN5ifrI4Gh5kS+G/UUqbKU4TNWE=', publicKey: 'zyQcH+swlgDiBwzDAtFnTMKWR//DdNPyFYNLLMOLsjc='} 98 | ] 99 | , 'interactive naclEncryptionKeyPairsBase64') 100 | }) 101 | }) 102 | 103 | test('interactive naclSigningKeyPairsBase64 test', function (t) { 104 | sessionKeys.generate('user@example.com', 'pet sprain our trial patch bg', function (err, key) { 105 | t.plan(9) 106 | 107 | t.notOk(err, 'interactive naclSigningKeyPairsBase64 no error') 108 | 109 | // NOTE : Test only the public (verify) key against the Ruby gem output. 110 | // RbNaCl and tweetnacl-js somehow have different private key values, 111 | // but the same public key values. This is OK, since the private 112 | // key is never shared. 113 | t.equals(key.naclSigningKeyPairsBase64[0]['publicKey'], '9mc6TmR6fw+OfPZA+TI4pDMeensYo3vHjCAWwNJr5Sg=', 'public key 0') 114 | t.equals(key.naclSigningKeyPairsBase64[1]['publicKey'], 'IA4yoeU/2xv2elvmJWFLP3Hiy2Hp5FdGpdrJjkf+5FU=', 'public key 1') 115 | t.equals(key.naclSigningKeyPairsBase64[2]['publicKey'], '/orhEpXoWrbHQcWlg/IEnNiJvW+2j0lS7+/gvOFlppc=', 'public key 2') 116 | t.equals(key.naclSigningKeyPairsBase64[3]['publicKey'], 'JAr8Pcij0HTvraNF9UeZ3vx0rvtixv4aIIMuDXrq+xc=', 'public key 3') 117 | t.equals(key.naclSigningKeyPairsBase64[4]['publicKey'], 'j9v+uOiZ2iVYwIiSMEeh8LVtFVagKkQ0n8lM6g7NIEY=', 'public key 4') 118 | t.equals(key.naclSigningKeyPairsBase64[5]['publicKey'], 'SJF1MvZ0Ggb1UQWHKkn9NHAkHU9A/ofV159fCsB6Pbo=', 'public key 5') 119 | t.equals(key.naclSigningKeyPairsBase64[6]['publicKey'], 'KlYHO13rFU3vlWxHvMo0s7nILVH6rzsgDAblSz3yVaw=', 'public key 6') 120 | t.equals(key.naclSigningKeyPairsBase64[7]['publicKey'], 'HllzWBHwzXC4XnQU0xSzGDYN5aUhD2lbSQJVF3f2mQA=', 'public key 7') 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sessionKeys (JavaScript) 2 | 3 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 4 | 5 | [![Build Status](https://travis-ci.org/grempe/session-keys-js.svg?branch=master)](https://travis-ci.org/grempe/session-keys-js) 6 | 7 | `sessionKeys` is a cryptographic tool for the generation of unique user IDs, 8 | and NaCl compatible [Curve25519](https://cr.yp.to/ecdh.html) encryption, and 9 | [Ed25519](http://ed25519.cr.yp.to) digital signature keys using JavaScript. 10 | 11 | It is compatible with [grempe/session-keys-rb](https://github.com/grempe/session-keys-rb) 12 | which can generates identical IDs and crypto keys server-side using Ruby when given the 13 | same username and passphrase values. Both libraries have extensive tests to 14 | ensure they remain interoperable. 15 | 16 | ## Security 17 | 18 | The encryption and signing keys are created with 19 | [TweetNaCl.js](https://github.com/dchest/tweetnacl-js), a port of 20 | [TweetNaCl](http://tweetnacl.cr.yp.to/) / [NaCl](http://nacl.cr.yp.to/) to 21 | JavaScript for modern browsers and Node.js. The encryption keys are for the 22 | Public-key authenticated encryption `box` construction which 23 | implements `curve25519-xsalsa20-poly1305`. The signing keys are for the `Ed25519` 24 | digital signature system. 25 | 26 | The strength of the system lies in the fact that the keypairs are derived from 27 | passing an identifier such as a username or email address, and a high-entropy 28 | passphrase through the `SHA256` cryptographic one-way hash function, 29 | and then 'stretching' that username/password into strong key material using 30 | the `scrypt` key derivation function. 31 | 32 | The benefit of this approach are manifold: 33 | 34 | - Cryptographically secure key generation, full 32 byte (256 bit) keys 35 | - Risk of brute force attempts at key discovery likely eliminated 36 | - A deterministic ID protects user privacy when used in place of stored username on server 37 | - No need to manage or move keypairs around for use on different devices 38 | - Users never need to store sensitive key material on disk 39 | - Key material can't be stolen or copied without compromise of username/passphrase 40 | - Key material is deterministic, same username/passphrase always results in same keys 41 | - Multiple sets of key material are generated, allowing applications to secure different things with different keys 42 | - Cross language, Javascript and Ruby currently supported. 43 | 44 | The code is simple and easily auditable, and uses only the fast and secure `SHA256` 45 | hash function, `scrypt` for strong key derivation, and the `NaCL` compatible 46 | encryption and digital signature keys provided by `tweetnacl-js`. 47 | 48 | This code was inspired by, but is **incompatible** with, the 49 | [session25519](https://github.com/jo/session25519) library created by 50 | [Johannes Jörg Schmidt (@jo)](https://github.com/jo). 51 | 52 | It bears repeating that **the strength of this system is very strongly 53 | tied to the strength of the passphrase chosen by the user**. Application 54 | developers are **strongly encouraged** to enforce the use of 55 | high-entropy passphrases by their users. Memorable high-entropy passphrases, 56 | such as can be generated using [Diceware](https://www.rempe.us/diceware/), 57 | and measured with password strength estimation tools like 58 | [zxcvbn](https://github.com/dropbox/zxcvbn), are critically important to 59 | the overall security of this system. 60 | 61 | ## Usage 62 | 63 | Simply pass in a user identifier, such as an email address and a high-entropy 64 | passphrase. The callback will return an Object Literal with the key material. 65 | 66 | A total of 256 bytes of key material is derived, and this is split into 8 67 | 32 byte keys which are returned as an Array of [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) 68 | objects with raw binary data. 69 | 70 | Each of those keys are also returned as hex values, and derived encryption 71 | and signing keypairs with Base64 encoded versions as well for convenience. 72 | This gives you eight different secure keys to choose from with various 73 | representations. 74 | 75 | 76 | ```js 77 | var sessionKeys = require('session-keys') 78 | 79 | sessionKeys.generate('user@example.com', 'my secret password', function(err, keys) { 80 | // { 81 | // id: "0123456789abcdef", 82 | // byteKeys: [...], 83 | // hexKeys: [...], 84 | // naclEncryptionKeyPairs: [...], 85 | // naclEncryptionKeyPairsBase64: [...], 86 | // naclSigningKeyPairs: [...], 87 | // naclSigningKeyPairsBase64: [...], 88 | // } 89 | }) 90 | ``` 91 | 92 | ## Cryptographic Design 93 | 94 | The following pseudo-code illustrates how `sessionKeys` derives keys 95 | from a user ID and passphrase. 96 | 97 | ```txt 98 | // PSEUDOCODE 99 | 100 | // 32 Byte hash of the user name 101 | id = SHA256(username) 102 | 103 | // 32 Byte hash of the password 104 | key = SHA256(password) 105 | 106 | // 32 Byte hash of the ID, its length, and a library specific string 107 | salt = SHA256(id + idLength + 'session_keys') 108 | 109 | // scrypt params 110 | // 256 bytes of scrypt output 111 | derivedBytes = scrypt(key, salt, N = 16384, r = 8, p = 1, dkLen = 256) 112 | 113 | // Return all of the following 114 | ////////////////////////////// 115 | 116 | // The hex encoded sha256(username) 117 | idhex = hex(id) 118 | 119 | // Split the 256 derived bytes into 8 * 32 byte Uint8Array keys 120 | byteKeys = [] 121 | 122 | // For each byteKey generate: 123 | 124 | // An Array of the hex values of each byteKey 125 | hexKeys = [] 126 | 127 | // An Array of NaCl Encryption keys seeded from each byteKey 128 | naclEncryptionKeyPairs = [] 129 | 130 | // An Array of NaCl Encryption keys seeded from each byteKey, Base 64 encoded 131 | naclEncryptionKeyPairsBase64 = [] 132 | 133 | // An Array of NaCl Signing keys seeded from each byteKey 134 | naclSigningKeyPairs = [] 135 | 136 | // An Array of NaCl Signing keys seeded from each byteKey, Base 64 encoded 137 | naclSigningKeyPairsBase64 = [] 138 | 139 | ``` 140 | 141 | ## Performance 142 | 143 | The author of [scrypt-async-js](https://github.com/dchest/scrypt-async-js), 144 | which is the strong key derivation mechanism used by `sessionKeys`, [recommends](https://github.com/dchest/scrypt-async-js/commit/ac57f235b505eb3f4fa8f2f95ae22d7eddd655d5) 145 | using `setImmediate`: 146 | 147 | > Using `setImmediate` massively improves performance. Since 148 | > most browsers don't support it, you'll have to include a 149 | > shim for it. 150 | 151 | - [YuzuJS/setImmediate](https://github.com/YuzuJS/setImmediate) 152 | - [setImmediate shim demo](http://jphpsf.github.io/setImmediate-shim-demo/) 153 | - [caniuse setImmediate](http://caniuse.com/#search=setImmediate) 154 | 155 | Performance in this context is *not* about making the key derivation run faster, as 156 | that would kind of defeat the purpose. It is instead about ensuring your 157 | application remains responsive while this code is running. 158 | 159 | ## Resources 160 | 161 | ### fast-sha256-js 162 | - Origin: https://github.com/dchest/fast-sha256-js 163 | - License: Public Domain 164 | 165 | ### scrypt-async-js 166 | - Origin: https://github.com/dchest/scrypt-async-js 167 | - License: BSD-like, see LICENSE file or MIT license at your choice. 168 | 169 | ### TweetNaCl.js 170 | - Origin: https://github.com/dchest/tweetnacl-js 171 | - License: Public Domain 172 | 173 | ### base64-js 174 | - Origin: https://github.com/beatgammit/base64-js 175 | - License: MIT 176 | 177 | ## Development 178 | 179 | ### Setup 180 | 181 | This project now manages all dependencies with [yarn](https://yarnpkg.com) which 182 | you'll need to install first. 183 | 184 | Make sure you are using v0.16.0 or higher. 185 | 186 | ``` 187 | $ yarn -V 188 | 0.16.0 189 | ``` 190 | 191 | Install all dependencies locally. 192 | 193 | ``` 194 | yarn 195 | ``` 196 | 197 | ### Build 198 | 199 | You can build a `dist` version of `sessionKeys` using `browserify`. There is a 200 | pre-built version in the `dist` directory of this repository which includes 201 | all dependencies and can be used with a `