Merge branch 'feature/drop-node-6'

This commit is contained in:
Claudius 2019-05-14 14:43:31 +02:00
commit 4c90863f2c
7 changed files with 134 additions and 48 deletions

View File

@ -2,7 +2,7 @@
"presets": [ "presets": [
["env", { ["env", {
"targets": { "targets": {
"node": "6", "node": "8",
"uglify": true "uglify": true
} }
}] }]

View File

@ -1,40 +1,40 @@
language: node_js language: node_js
dist: trusty dist: xenial
cache: yarn cache: yarn
env:
global:
- CXX=g++-4.8
- YARN_VERSION=1.15.2
jobs: jobs:
include: include:
- env: task=npm-test - stage: Static Tests
node_js: name: eslint
- 6
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
- export PATH="$HOME/.yarn/bin:$PATH"
- env: task=npm-test
node_js:
- 8
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
- export PATH="$HOME/.yarn/bin:$PATH"
- env: task=npm-test
node_js: node_js:
- 10 - 10
before_install: script:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION" - yarn run eslint
- export PATH="$HOME/.yarn/bin:$PATH" - name: ShellCheck
- env: task=ShellCheck
script: script:
- shellcheck bin/heroku bin/setup - shellcheck bin/heroku bin/setup
language: generic language: generic
- env: task=json-lint - name: json-lint
addons: addons:
apt: apt:
packages: packages:
- jq - jq
script: script:
- npm run jsonlint - yarn run jsonlint
language: generic language: generic
- stage: Dynamic Tests
name: Node.js 8
node_js:
- 8
script:
- yarn run mocha-suite
- name: Node.js 10
node_js:
- 10
script:
- yarn run mocha-suite
- name: Node.js 12
node_js:
- 12
script:
- yarn run mocha-suite

View File

@ -3,11 +3,10 @@ Manual Installation
## Requirements on your server ## Requirements on your server
- Node.js 6.x or up (test up to 7.5.0) and <10.x - Node.js 8.5 or up
- Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) use charset `utf8` - Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) use charset `utf8`
- npm (and its dependencies, [node-gyp](https://github.com/nodejs/node-gyp#installation)) - npm (and its dependencies, [node-gyp](https://github.com/nodejs/node-gyp#installation))
- yarn - yarn
- `libssl-dev` for building scrypt (see [here](https://github.com/ml1nk/node-scrypt/blob/master/README.md#installation-instructions) for further information)
- Bash (for the setup script) - Bash (for the setup script)
- For **building** CodiMD we recommend to use a machine with at least **2GB** RAM - For **building** CodiMD we recommend to use a machine with at least **2GB** RAM

View File

@ -1,11 +1,20 @@
'use strict' 'use strict'
// external modules // external modules
var Sequelize = require('sequelize') const Sequelize = require('sequelize')
var scrypt = require('@mlink/scrypt') const crypto = require('crypto')
if (!crypto.scrypt) {
// polyfill for node.js 8.0, see https://github.com/chrisveness/scrypt-kdf#openssl-implementation
const scryptAsync = require('scrypt-async')
crypto.scrypt = function (password, salt, keylen, options, callback) {
const opt = Object.assign({}, options, { dkLen: keylen })
scryptAsync(password, salt, opt, (derivedKey) => callback(null, Buffer.from(derivedKey)))
}
}
const scrypt = require('scrypt-kdf')
// core // core
var logger = require('../logger') const logger = require('../logger')
var {generateAvatarURL} = require('../letter-avatars') const { generateAvatarURL } = require('../letter-avatars')
module.exports = function (sequelize, DataTypes) { module.exports = function (sequelize, DataTypes) {
var User = sequelize.define('User', { var User = sequelize.define('User', {
@ -41,20 +50,12 @@ module.exports = function (sequelize, DataTypes) {
} }
}, },
password: { password: {
type: Sequelize.TEXT, type: Sequelize.TEXT
set: function (value) {
var hash = scrypt.kdfSync(value, scrypt.paramsSync(0.1)).toString('hex')
this.setDataValue('password', hash)
}
} }
}, { }, {
instanceMethods: { instanceMethods: {
verifyPassword: function (attempt) { verifyPassword: function (attempt) {
if (scrypt.verifyKdfSync(Buffer.from(this.password, 'hex'), attempt)) { return scrypt.verify(Buffer.from(this.password, 'hex'), attempt)
return this
} else {
return false
}
} }
}, },
classMethods: { classMethods: {
@ -153,5 +154,19 @@ module.exports = function (sequelize, DataTypes) {
} }
}) })
function updatePasswordHashHook (user, options, done) {
// suggested way to hash passwords to be able to do this asynchronously:
// @see https://github.com/sequelize/sequelize/issues/1821#issuecomment-44265819
if (!user.changed('password')) { return done() }
scrypt.kdf(user.getDataValue('password'), { logN: 15 }).then(keyBuf => {
user.setDataValue('password', keyBuf.toString('hex'))
done()
})
}
User.beforeCreate(updatePasswordHashHook)
User.beforeUpdate(updatePasswordHashHook)
return User return User
} }

View File

@ -23,8 +23,14 @@ passport.use(new LocalStrategy({
} }
}).then(function (user) { }).then(function (user) {
if (!user) return done(null, false) if (!user) return done(null, false)
if (!user.verifyPassword(password)) return done(null, false) user.verifyPassword(password).then(verified => {
return done(null, user) if (verified) {
return done(null, user)
} else {
logger.warn('invalid password given for %s', user.email)
return done(null, false)
}
})
}).catch(function (err) { }).catch(function (err) {
logger.error(err) logger.error(err)
return done(err) return done(err)

View File

@ -5,9 +5,10 @@
"main": "app.js", "main": "app.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"test": "npm run-script eslint && npm run-script jsonlint && mocha", "test": "npm run-script eslint && npm run-script jsonlint && npm run-script mocha-suite",
"eslint": "node_modules/.bin/eslint lib public test app.js", "eslint": "node_modules/.bin/eslint lib public test app.js",
"jsonlint": "find . -not -path './node_modules/*' -type f -name '*.json' -o -type f -name '*.json.example' | while read json; do echo $json ; jq . $json; done", "jsonlint": "find . -not -path './node_modules/*' -type f -name '*.json' -o -type f -name '*.json.example' | while read json; do echo $json ; jq . $json; done",
"mocha-suite": "NODE_ENV=test CMD_DB_URL=\"sqlite::memory:\" mocha --exit",
"standard": "echo 'standard is no longer being used, use `npm run eslint` instead!' && exit 1", "standard": "echo 'standard is no longer being used, use `npm run eslint` instead!' && exit 1",
"dev": "webpack --config webpack.dev.js --progress --colors --watch", "dev": "webpack --config webpack.dev.js --progress --colors --watch",
"heroku-prebuild": "bin/heroku", "heroku-prebuild": "bin/heroku",
@ -57,7 +58,6 @@
"jquery-ui": "^1.12.1", "jquery-ui": "^1.12.1",
"js-cookie": "^2.1.3", "js-cookie": "^2.1.3",
"js-sequence-diagrams": "git+https://github.com/codimd/js-sequence-diagrams.git", "js-sequence-diagrams": "git+https://github.com/codimd/js-sequence-diagrams.git",
"wurl": "^2.5.3",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"jsdom-nogyp": "^0.8.3", "jsdom-nogyp": "^0.8.3",
"keymaster": "^1.6.2", "keymaster": "^1.6.2",
@ -110,7 +110,8 @@
"readline-sync": "^1.4.7", "readline-sync": "^1.4.7",
"request": "^2.88.0", "request": "^2.88.0",
"reveal.js": "~3.7.0", "reveal.js": "~3.7.0",
"@mlink/scrypt": "^6.1.2", "scrypt-async": "^2.0.1",
"scrypt-kdf": "^2.0.1",
"select2": "^3.5.2-browserify", "select2": "^3.5.2-browserify",
"sequelize": "^3.28.0", "sequelize": "^3.28.0",
"sequelize-cli": "^2.5.1", "sequelize-cli": "^2.5.1",
@ -118,7 +119,7 @@
"socket.io": "~2.1.1", "socket.io": "~2.1.1",
"socket.io-client": "~2.1.1", "socket.io-client": "~2.1.1",
"spin.js": "^2.3.2", "spin.js": "^2.3.2",
"sqlite3": "^4.0.1", "sqlite3": "^4.0.7",
"store": "^2.0.12", "store": "^2.0.12",
"string": "^3.3.3", "string": "^3.3.3",
"tedious": "^1.14.0", "tedious": "^1.14.0",
@ -131,6 +132,7 @@
"viz.js": "^1.7.0", "viz.js": "^1.7.0",
"winston": "^3.1.0", "winston": "^3.1.0",
"ws": "^6.0.0", "ws": "^6.0.0",
"wurl": "^2.5.3",
"xss": "^1.0.3" "xss": "^1.0.3"
}, },
"resolutions": { "resolutions": {
@ -139,7 +141,7 @@
"**/request": "^2.88.0" "**/request": "^2.88.0"
}, },
"engines": { "engines": {
"node": ">=6.x" "node": ">=8.x"
}, },
"bugs": "https://github.com/codimd/server/issues", "bugs": "https://github.com/codimd/server/issues",
"keywords": [ "keywords": [

64
test/user.js Normal file
View File

@ -0,0 +1,64 @@
/* eslint-env node, mocha */
'use strict'
const assert = require('assert')
const models = require('../lib/models')
const User = models.User
describe('User Sequelize model', function () {
beforeEach(() => {
return models.sequelize.sync({ force: true })
})
it('stores a password hash on creation and verifies that password', function () {
const userData = {
password: 'test123'
}
const intentionallyInvalidPassword = 'stuff'
return User.create(userData).then(u => {
return Promise.all([
u.verifyPassword(userData.password).then(result => assert.strictEqual(result, true)),
u.verifyPassword(intentionallyInvalidPassword).then(result => assert.strictEqual(result, false))
]).catch(e => assert.fail(e))
})
})
it('can cope with password stored in standard scrypt header format', function () {
const testKey = '736372797074000e00000008000000018c7b8c1ac273fd339badde759b3efc418bc61b776debd02dfe95989383cf9980ad21d2403dce33f4b551f5e98ce84edb792aee62600b1303ab8d4e6f0a53b0746e73193dbf557b888efc83a2d6a055a9'
const validPassword = 'test'
const intentionallyInvalidPassword = 'stuff'
const u = User.build()
u.setDataValue('password', testKey) // this circumvents the setter - which we don't need in this case!
return Promise.all([
u.verifyPassword(validPassword).then(result => assert.strictEqual(result, true)),
u.verifyPassword(intentionallyInvalidPassword).then(result => assert.strictEqual(result, false))
]).catch(e => assert.fail(e))
})
it('deals with various characters correctly', function () {
const combinations = [
// ['correct password', 'scrypt syle hash']
['test', '736372797074000e00000008000000018c7b8c1ac273fd339badde759b3efc418bc61b776debd02dfe95989383cf9980ad21d2403dce33f4b551f5e98ce84edb792aee62600b1303ab8d4e6f0a53b0746e73193dbf557b888efc83a2d6a055a9'],
['ohai', '736372797074000e00000008000000010efec4e5ce6a5294491f1b1cccc38d3562f84844b9271aef635f8bc338cf4e0e0bac62ebb11379e85894c1f694e038fc39b087b4fdacd1280b50a7382d7ffbfc82f2190bef70d47708d2a94b75126294'],
['my secret pw', '736372797074000f0000000800000001ffb4cd10a1dfe9e64c1e5416fd6d55b390b6822e78b46fd1f963fe9f317a1e05f9c5fee15e1f618286f4e38b55364ae1e7dc295c9dc33ee0f5712e86afe37e5784ff9c7cf84cf0e631dd11f84f3621e7'],
['my secret pw', /* different hash! */ '736372797074000f0000000800000001f6083e9593365acd07550f7c72f19973fb7d52c3ef0a78026ff66c48ab14493843c642167b5e6b7f31927e8eeb912bc2639e41955fae15da5099998948cfeacd022f705624931c3b30104e6bb296b805'],
['i am so extremely long, it\'s not even funny. Wait, you\'re still reading?', '736372797074000f00000008000000012d205f7bb529bb3a8b8bb25f5ab46197c7e9baf1aad64cf5e7b2584c84748cacf5e60631d58d21cb51fa34ea93b517e2fe2eb722931db5a70ff5a1330d821288ee7380c4136369f064b71b191a785a5b']
]
const intentionallyInvalidPassword = 'stuff'
return Promise.all(combinations.map((combination, index) => {
const u = User.build()
u.setDataValue('password', combination[1])
return Promise.all([
u.verifyPassword(combination[0])
.then(result => assert.strictEqual(result, true, `password #${index} "${combination[0]}" should have been verified`)),
u.verifyPassword(intentionallyInvalidPassword)
.then(result => assert.strictEqual(result, false, `password #${index} "${combination[0]}" should NOT have been verified`))
])
})).catch(e => assert.fail(e))
})
})