@@ -1,3 +0,0 @@ | |||||
language: node_js | |||||
node_js: | |||||
- 4 |
@@ -1,7 +1,6 @@ | |||||
[![Build Status](https://travis-ci.org/oslab-fr/lesspass.svg?branch=master)](https://travis-ci.org/oslab-fr/lesspass) | |||||
# lesspass | # lesspass | ||||
lesspass is like keepass without the need to persist passwords | |||||
lesspass single page app for https://lesspass.com | |||||
## requirements | ## requirements | ||||
@@ -23,19 +22,6 @@ start application | |||||
open the application in a browser: [http://localhost:8080](http://localhost:8080) | open the application in a browser: [http://localhost:8080](http://localhost:8080) | ||||
## run tests | |||||
install dependencies | |||||
npm install | |||||
run tests | |||||
npm run test | |||||
run test in watch mode | |||||
npm run test:watch | |||||
## run production mode | ## run production mode | ||||
@@ -1,38 +0,0 @@ | |||||
<style> | |||||
#toast-container > div { | |||||
opacity: 1; | |||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100); | |||||
filter: alpha(opacity=100); | |||||
} | |||||
body { | |||||
font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; | |||||
font-size: 14px; | |||||
line-height: 1.4; | |||||
background: #D3D8E8; | |||||
color: #252830; | |||||
} | |||||
.blue { | |||||
color: #0275D8; | |||||
} | |||||
</style> | |||||
<template> | |||||
<div> | |||||
<lesspass-header></lesspass-header> | |||||
<router-view></router-view> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import LesspassHeader from './components/header.vue'; | |||||
export default { | |||||
data: function () { | |||||
return {} | |||||
}, | |||||
components: { | |||||
LesspassHeader | |||||
} | |||||
} | |||||
</script> |
@@ -1,29 +0,0 @@ | |||||
<template> | |||||
<lesspass-headlines></lesspass-headlines> | |||||
<password-generator></password-generator> | |||||
<lesspass-features></lesspass-features> | |||||
<lesspass-faq></lesspass-faq> | |||||
<lesspass-footer></lesspass-footer> | |||||
</template> | |||||
<script> | |||||
import LesspassHeadlines from './headlines.vue'; | |||||
import PasswordGenerator from './password-generator.vue'; | |||||
import LesspassFeatures from './features.vue'; | |||||
import LesspassFaq from './faq.vue'; | |||||
import LesspassFooter from './footer.vue'; | |||||
export default { | |||||
data: function () { | |||||
return {} | |||||
}, | |||||
components: { | |||||
LesspassHeadlines, | |||||
PasswordGenerator, | |||||
LesspassFeatures, | |||||
LesspassFaq, | |||||
LesspassFooter | |||||
} | |||||
} | |||||
</script> |
@@ -140,7 +140,7 @@ | |||||
</template> | </template> | ||||
<script> | <script> | ||||
import Lesspass from '../lesspass' | |||||
import lesspass from 'lesspass' | |||||
import Clipboard from 'clipboard'; | import Clipboard from 'clipboard'; | ||||
export default { | export default { | ||||
@@ -163,7 +163,7 @@ | |||||
var email = this.email; | var email = this.email; | ||||
var password = this.password; | var password = this.password; | ||||
if (email && password) { | if (email && password) { | ||||
Lesspass.createMasterPassword(email, password).then(function (masterPassword) { | |||||
lesspass.createMasterPassword(email, password).then(function (masterPassword) { | |||||
self.$set('masterPassword', masterPassword); | self.$set('masterPassword', masterPassword); | ||||
}); | }); | ||||
} | } | ||||
@@ -185,7 +185,7 @@ | |||||
site: site, | site: site, | ||||
password: this.passwordInfo | password: this.passwordInfo | ||||
}; | }; | ||||
return Lesspass.createPassword(masterPassword, entry); | |||||
return lesspass.createPassword(masterPassword, entry); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -1,84 +0,0 @@ | |||||
import crypto from 'crypto'; | |||||
export default class lesspass { | |||||
static createPassword(masterPassword, entry) { | |||||
var hash = lesspass._createHash(masterPassword, entry); | |||||
var template = lesspass._getTemplate(entry.password.settings); | |||||
return lesspass._encode(hash, template); | |||||
} | |||||
static createMasterPassword(email, password) { | |||||
return new Promise((resolve, reject) => { | |||||
var iterations = 8192; | |||||
var keylen = 32; | |||||
crypto.pbkdf2(password, email, iterations, keylen, 'sha256', function (error, key) { | |||||
if (error) { | |||||
reject('error in pbkdf2'); | |||||
} else { | |||||
resolve(key.toString('hex')); | |||||
} | |||||
}); | |||||
}); | |||||
} | |||||
static _createHash(masterPassword, {site, password={length: 12, counter: 1}}) { | |||||
var salt = site + password.counter.toString(); | |||||
var hash = crypto.createHmac('sha256', masterPassword).update(salt).digest('hex'); | |||||
return hash.substring(0, password.length); | |||||
} | |||||
static _getTemplate(passwordTypes = ['strong']) { | |||||
var passwordTypesInfo = { | |||||
lowercase: {value: 'vc', order: 1}, | |||||
uppercase: {value: 'VC', order: 2}, | |||||
numbers: {value: 'n', order: 3}, | |||||
symbols: {value: 's', order: 4}, | |||||
strong: {value: 'Cvcvns', order: 5} | |||||
}; | |||||
return passwordTypes | |||||
.map(passwordType => passwordTypesInfo[passwordType]) | |||||
.sort((passwordType1, passwordType2) => passwordType1.order > passwordType2.order) | |||||
.map(passwordType => passwordType.value) | |||||
.join(''); | |||||
} | |||||
static _string2charCodes(text) { | |||||
var charCodes = []; | |||||
for (let i = 0; i < text.length; i++) { | |||||
charCodes.push(text.charCodeAt(i)); | |||||
} | |||||
return charCodes; | |||||
} | |||||
static _getCharType(template, index) { | |||||
return template[index % template.length]; | |||||
} | |||||
static _getPasswordChar(charType, index) { | |||||
var passwordsChars = { | |||||
V: "AEIOUY", | |||||
C: "BCDFGHJKLMNPQRSTVWXZ", | |||||
v: "aeiouy", | |||||
c: "bcdfghjklmnpqrstvwxz", | |||||
A: "AEIOUYBCDFGHJKLMNPQRSTVWXZ", | |||||
a: "AEIOUYaeiouyBCDFGHJKLMNPQRSTVWXZbcdfghjklmnpqrstvwxz", | |||||
n: "0123456789", | |||||
s: "@&%?,=[]_:-+*$#!'^~;()/.", | |||||
x: "AEIOUYaeiouyBCDFGHJKLMNPQRSTVWXZbcdfghjklmnpqrstvwxz0123456789@&%?,=[]_:-+*$#!'^~;()/." | |||||
}; | |||||
var passwordChar = passwordsChars[charType]; | |||||
return passwordChar[index % passwordChar.length]; | |||||
} | |||||
static _encode(hash, template) { | |||||
var password = ''; | |||||
this._string2charCodes(hash).map( | |||||
(charCode, index) => { | |||||
let charType = this._getCharType(template, index); | |||||
password += this._getPasswordChar(charType, charCode); | |||||
} | |||||
); | |||||
return password; | |||||
} | |||||
} |
@@ -1,51 +0,0 @@ | |||||
import Vue from 'vue' | |||||
import Router from 'vue-router'; | |||||
import Resource from 'vue-resource'; | |||||
import i18n from 'vue-i18n'; | |||||
import locales from './locales/locales'; | |||||
import App from './app.vue'; | |||||
import IndexView from './components/index.vue'; | |||||
import Dashboard from './components/dashboard.vue'; | |||||
import auth from './services/auth'; | |||||
Vue.use(Resource); | |||||
Vue.use(Router); | |||||
var browserLanguage = (navigator.language || navigator.browserLanguage).split('-')[0]; | |||||
var lang = browserLanguage in locales ? browserLanguage : 'en'; | |||||
Vue.use(i18n, { | |||||
lang: lang, | |||||
locales: locales | |||||
}); | |||||
Vue.http.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('token'); | |||||
auth.checkAuth(); | |||||
export var router = new Router(); | |||||
router.map({ | |||||
'/': { | |||||
auth: true, | |||||
component: Dashboard | |||||
}, | |||||
'/presentation/': { | |||||
component: IndexView | |||||
} | |||||
}); | |||||
router.redirect({ | |||||
'*': '/' | |||||
}); | |||||
router.start(App, '#app'); | |||||
router.beforeEach(function (transition) { | |||||
if (transition.to.auth && !auth.user.authenticated) { | |||||
transition.redirect('/presentation/') | |||||
} else { | |||||
transition.next() | |||||
} | |||||
}); |
@@ -1,11 +1,9 @@ | |||||
{ | { | ||||
"name": "lesspass", | |||||
"name": "lesspass.com", | |||||
"version": "1.0.0", | "version": "1.0.0", | ||||
"description": "lesspass is like keepass without the need to persist passwords", | |||||
"description": "lesspass single page app for https://lesspass.com", | |||||
"main": "app/app.js", | "main": "app/app.js", | ||||
"scripts": { | "scripts": { | ||||
"test": "mocha --compilers js:babel-core/register tests", | |||||
"test:watch": "npm run test -- -w", | |||||
"predev": "npm install", | "predev": "npm install", | ||||
"dev": "webpack-dev-server --inline --hot --host 0.0.0.0", | "dev": "webpack-dev-server --inline --hot --host 0.0.0.0", | ||||
"prebuild": "rimraf dist && npm prune && npm install", | "prebuild": "rimraf dist && npm prune && npm install", | ||||
@@ -32,6 +30,7 @@ | |||||
"express": "^4.13.4", | "express": "^4.13.4", | ||||
"font-awesome": "^4.5.0", | "font-awesome": "^4.5.0", | ||||
"jquery": "^2.2.0", | "jquery": "^2.2.0", | ||||
"lesspass": "^1.1.1", | |||||
"tether": "^1.1.1", | "tether": "^1.1.1", | ||||
"toastr": "^2.1.2", | "toastr": "^2.1.2", | ||||
"vue": "^1.0.16", | "vue": "^1.0.16", | ||||
@@ -1,176 +0,0 @@ | |||||
import assert from 'assert'; | |||||
import Lesspass from '../app/lesspass'; | |||||
describe('LessPass', ()=> { | |||||
describe('public api', ()=> { | |||||
it('should create password', function () { | |||||
var masterPassword = "password"; | |||||
var entry = { | |||||
site: 'facebook', | |||||
password: { | |||||
length: 14, | |||||
settings: ['lowercase', 'uppercase', 'numbers', 'symbols'], | |||||
counter: 1 | |||||
} | |||||
}; | |||||
assert.equal('iwIQ8[acYT4&oc', Lesspass.createPassword(masterPassword, entry)); | |||||
}); | |||||
it('should create password 2', function () { | |||||
var masterPassword = "password"; | |||||
var entry = { | |||||
site: 'facebook', | |||||
password: { | |||||
length: 12, | |||||
settings: ['strong'], | |||||
counter: 1 | |||||
} | |||||
}; | |||||
assert.equal('Vexu8[Syce4&', Lesspass.createPassword(masterPassword, entry)); | |||||
}); | |||||
it('should create 2 passwords different if counter different', function () { | |||||
var masterPassword = "password"; | |||||
var entry = { | |||||
site: 'facebook', | |||||
password: { | |||||
length: 14, | |||||
settings: ['lowercase', 'uppercase', 'numbers', 'symbols'], | |||||
counter: 1 | |||||
} | |||||
}; | |||||
var entry2 = { | |||||
site: 'facebook', | |||||
password: { | |||||
length: 14, | |||||
settings: ['lowercase', 'uppercase', 'numbers', 'symbols'], | |||||
counter: 2 | |||||
} | |||||
}; | |||||
assert.notEqual( | |||||
Lesspass.createPassword(masterPassword, entry), | |||||
Lesspass.createPassword(masterPassword, entry2) | |||||
); | |||||
}); | |||||
it('should create master password with pbkdf2 (8192 iterations and sha 256)', (done)=> { | |||||
var email = 'test@lesspass.com'; | |||||
var password = "password"; | |||||
Lesspass.createMasterPassword(email, password).then((masterPassword) => { | |||||
assert.equal("90cff82b8847525370a8f29a59ecf45db62c719a535788ad0df58d32304e925d", masterPassword); | |||||
assert.equal(64, masterPassword.length); | |||||
done(); | |||||
}); | |||||
}); | |||||
it('should create 64 char length master password', (done)=> { | |||||
var email = 'test@lesspass.com'; | |||||
var password = "password"; | |||||
Lesspass.createMasterPassword(email, password).then((masterPassword) => { | |||||
assert.equal("90cff82b8847525370a8f29a59ecf45db62c719a535788ad0df58d32304e925d", masterPassword); | |||||
assert.equal(64, masterPassword.length); | |||||
done(); | |||||
}); | |||||
}); | |||||
}); | |||||
describe('hash', ()=> { | |||||
it('should have default length of 12', ()=> { | |||||
var masterPassword = 'password'; | |||||
var entry = {'site': 'facebook'}; | |||||
assert.equal(12, Lesspass._createHash(masterPassword, entry).length); | |||||
}); | |||||
it('should be able to create hash with defined length', ()=> { | |||||
var masterPassword = 'password'; | |||||
var entry = { | |||||
site: 'facebook', | |||||
password: { | |||||
length: 10, | |||||
counter: 1 | |||||
} | |||||
}; | |||||
assert.equal(10, Lesspass._createHash(masterPassword, entry).length); | |||||
}); | |||||
it('should return two different passwords if site different', ()=> { | |||||
var masterPassword = 'password'; | |||||
var entry = {site: 'facebook'}; | |||||
var entry2 = {site: 'google'}; | |||||
assert.notEqual( | |||||
Lesspass._createHash(masterPassword, entry), | |||||
Lesspass._createHash(masterPassword, entry2) | |||||
); | |||||
}); | |||||
it('should return two different passwords if counter different', ()=> { | |||||
var masterPassword = 'password'; | |||||
var entry = { | |||||
site: 'facebook', | |||||
password: { | |||||
length: 14, | |||||
settings: ['lowercase', 'uppercase', 'numbers', 'symbols'], | |||||
counter: 1 | |||||
} | |||||
}; | |||||
var entry2 = { | |||||
site: 'facebook', | |||||
password: { | |||||
length: 14, | |||||
settings: ['lowercase', 'uppercase', 'numbers', 'symbols'], | |||||
counter: 2 | |||||
} | |||||
}; | |||||
assert.notEqual( | |||||
Lesspass._createHash(masterPassword, entry), | |||||
Lesspass._createHash(masterPassword, entry2) | |||||
); | |||||
}); | |||||
}); | |||||
describe('password templates', ()=> { | |||||
it('should get default template from password type', ()=> { | |||||
assert.equal('Cvcvns', Lesspass._getTemplate()); | |||||
}); | |||||
it('should get template from password type', ()=> { | |||||
assert.equal('vc', Lesspass._getTemplate(['lowercase'])); | |||||
assert.equal('VC', Lesspass._getTemplate(['uppercase'])); | |||||
assert.equal('n', Lesspass._getTemplate(['numbers'])); | |||||
assert.equal('s', Lesspass._getTemplate(['symbols'])); | |||||
}); | |||||
it('should concatenate template if two password password_types', ()=> { | |||||
assert.equal('vcVC', Lesspass._getTemplate(['lowercase', 'uppercase'])); | |||||
assert.equal('vcns', Lesspass._getTemplate(['lowercase', 'numbers', 'symbols'])); | |||||
}); | |||||
it('should not care about order of type in password password_types', ()=> { | |||||
assert.equal( | |||||
Lesspass._getTemplate(['uppercase', 'lowercase']), | |||||
Lesspass._getTemplate(['lowercase', 'uppercase']) | |||||
); | |||||
}); | |||||
it('should return char inside template based on modulo of the index', function () { | |||||
var template = 'cv'; | |||||
assert.equal('c', Lesspass._getCharType(template, 0)); | |||||
assert.equal('v', Lesspass._getCharType(template, 1)); | |||||
assert.equal('c', Lesspass._getCharType(template, 10)); | |||||
}); | |||||
}); | |||||
describe('crypto', ()=> { | |||||
it('should convert a string into a char code table', ()=> { | |||||
var charCodes = Lesspass._string2charCodes('ab40f6ee71'); | |||||
assert.equal(97, charCodes[0]); | |||||
assert.equal(98, charCodes[1]); | |||||
assert.equal(10, charCodes.length); | |||||
}); | |||||
it('should return password size same size of hash given', function () { | |||||
var hash = 'Y2Vi2a112A'; | |||||
assert.equal(10, Lesspass._encode(hash, 'cv').length); | |||||
}); | |||||
it('should return different values if templates are different', function () { | |||||
var hash = 'a'; | |||||
assert.notEqual(Lesspass._encode(hash, 'cv'), Lesspass._encode(hash, 'vc')); | |||||
}); | |||||
it('should get password char based on its type and index', function () { | |||||
var typeVowel = 'V'; | |||||
assert.equal('A', Lesspass._getPasswordChar(typeVowel, 0)); | |||||
}); | |||||
it('should modulo if overflow', function () { | |||||
var typeVowel = 'V'; | |||||
assert.equal('E', Lesspass._getPasswordChar(typeVowel, 1)); | |||||
assert.equal('E', Lesspass._getPasswordChar(typeVowel, 7)); | |||||
}); | |||||
}); | |||||
}); |
@@ -8,24 +8,17 @@ module.exports = { | |||||
publicPath: '/dist/', | publicPath: '/dist/', | ||||
filename: "bundle.js" | filename: "bundle.js" | ||||
}, | }, | ||||
module: { | module: { | ||||
loaders: [ | loaders: [ | ||||
//{test: /\.js$/, loader: 'babel-loader', query: {presets: ['es2015']}}, | |||||
{ | |||||
test: /\.js$/, | |||||
exclude: /node_modules|vue\/dist|vue-router\/|vue-loader\/|vue-hot-reload-api\//, | |||||
loader: 'babel' | |||||
}, | |||||
{test: /\.js$/, loader: 'babel-loader', query: {presets: ['es2015']}}, | |||||
{test: /\.css$/, loader: 'style-loader!css-loader'}, | {test: /\.css$/, loader: 'style-loader!css-loader'}, | ||||
{test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}, | {test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}, | ||||
{test: /\.vue$/, loader: 'vue'}, | {test: /\.vue$/, loader: 'vue'}, | ||||
{ test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"}, | |||||
{ test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"}, | |||||
{ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream"}, | |||||
{test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"}, | |||||
{test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/font-woff"}, | |||||
{test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream"}, | |||||
{test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file"}, | {test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file"}, | ||||
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml"} | |||||
{test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml"} | |||||
] | ] | ||||
}, | }, | ||||
plugins: [ | plugins: [ | ||||
@@ -35,4 +28,13 @@ module.exports = { | |||||
"window.jQuery": "jquery" | "window.jQuery": "jquery" | ||||
}) | }) | ||||
] | ] | ||||
}; | |||||
}; | |||||
if (process.env.NODE_ENV === 'production') { | |||||
module.exports.plugins = [ | |||||
new webpack.DefinePlugin({'process.env': {NODE_ENV: '"production"'}}), | |||||
new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}, comments: false}), | |||||
new webpack.optimize.OccurenceOrderPlugin(), | |||||
new webpack.optimize.DedupePlugin() | |||||
] | |||||
} |
@@ -1,86 +0,0 @@ | |||||
var webpack = require('webpack'); | |||||
module.exports = { | |||||
entry: ['./app/main.js'], | |||||
output: { | |||||
path: './dist', | |||||
publicPath: '/dist/', | |||||
filename: 'app.js' | |||||
}, | |||||
devServer: { | |||||
port: 8080 | |||||
}, | |||||
module: { | |||||
loaders: [ | |||||
{ | |||||
test: /\.scss$/, | |||||
loaders: ['css', 'sass'] | |||||
}, | |||||
{ | |||||
test: /\.js$/, | |||||
exclude: /node_modules|vue\/dist|vue-router\/|vue-loader\/|vue-hot-reload-api\//, | |||||
loader: 'babel' | |||||
}, | |||||
{ | |||||
test: /\.vue$/, | |||||
loader: 'vue' | |||||
}, | |||||
{ | |||||
test: /\.(png|jpe?g|gif)$/, | |||||
loader: 'url', | |||||
query: { | |||||
limit: 10000, | |||||
name: '[name].[ext]?[hash]' | |||||
} | |||||
}, | |||||
{ | |||||
test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, | |||||
loader: "url?limit=10000&mimetype=application/font-woff" | |||||
}, | |||||
{ | |||||
test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, | |||||
loader: "url?limit=10000&mimetype=application/font-woff" | |||||
}, | |||||
{ | |||||
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, | |||||
loader: "url?limit=10000&mimetype=application/octet-stream" | |||||
}, | |||||
{ | |||||
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, | |||||
loader: "file" | |||||
}, | |||||
{ | |||||
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, | |||||
loader: "url?limit=10000&mimetype=image/svg+xml" | |||||
} | |||||
] | |||||
}, | |||||
babel: { | |||||
presets: ['es2015'], | |||||
plugins: ['transform-runtime'] | |||||
}, | |||||
plugins: [ | |||||
new webpack.ProvidePlugin({ | |||||
$: "jquery", | |||||
jQuery: "jquery", | |||||
"window.jQuery": "jquery" | |||||
}) | |||||
] | |||||
}; | |||||
if (process.env.NODE_ENV === 'production') { | |||||
module.exports.plugins = [ | |||||
new webpack.DefinePlugin({ | |||||
'process.env': { | |||||
NODE_ENV: '"production"' | |||||
} | |||||
}), | |||||
new webpack.optimize.UglifyJsPlugin({ | |||||
compress: { | |||||
warnings: false | |||||
}, | |||||
comments: false | |||||
}), | |||||
new webpack.optimize.OccurenceOrderPlugin() | |||||
] | |||||
} |