├── .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 | [](https://github.com/feross/standard)
4 |
5 | [](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 `