@@ -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 is like keepass without the need to persist passwords | |||
lesspass single page app for https://lesspass.com | |||
## requirements | |||
@@ -23,19 +22,6 @@ start application | |||
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 | |||
@@ -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> | |||
<script> | |||
import Lesspass from '../lesspass' | |||
import lesspass from 'lesspass' | |||
import Clipboard from 'clipboard'; | |||
export default { | |||
@@ -163,7 +163,7 @@ | |||
var email = this.email; | |||
var password = this.password; | |||
if (email && password) { | |||
Lesspass.createMasterPassword(email, password).then(function (masterPassword) { | |||
lesspass.createMasterPassword(email, password).then(function (masterPassword) { | |||
self.$set('masterPassword', masterPassword); | |||
}); | |||
} | |||
@@ -185,7 +185,7 @@ | |||
site: site, | |||
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", | |||
"description": "lesspass is like keepass without the need to persist passwords", | |||
"description": "lesspass single page app for https://lesspass.com", | |||
"main": "app/app.js", | |||
"scripts": { | |||
"test": "mocha --compilers js:babel-core/register tests", | |||
"test:watch": "npm run test -- -w", | |||
"predev": "npm install", | |||
"dev": "webpack-dev-server --inline --hot --host 0.0.0.0", | |||
"prebuild": "rimraf dist && npm prune && npm install", | |||
@@ -32,6 +30,7 @@ | |||
"express": "^4.13.4", | |||
"font-awesome": "^4.5.0", | |||
"jquery": "^2.2.0", | |||
"lesspass": "^1.1.1", | |||
"tether": "^1.1.1", | |||
"toastr": "^2.1.2", | |||
"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/', | |||
filename: "bundle.js" | |||
}, | |||
module: { | |||
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: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}, | |||
{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: /\.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: [ | |||
@@ -35,4 +28,13 @@ module.exports = { | |||
"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() | |||
] | |||
} |