Merge pull request #119 from whiteout-io/dev/WO-574

Skip setting passphrase in setup
This commit is contained in:
Felix Hammerl 2014-09-15 18:23:46 +02:00
commit 221bbea17c
9 changed files with 172 additions and 210 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ test/integration/src/
src/lib/*.js src/lib/*.js
src/js/crypto/aes-gcm.js src/js/crypto/aes-gcm.js
src/js/crypto/util.js src/js/crypto/util.js
.elasticbeanstalk/

View File

@ -9,9 +9,8 @@ define(function(require) {
states = { states = {
IDLE: 1, IDLE: 1,
SET_PASSPHRASE: 2, PROCESSING: 2,
PROCESSING: 3, DONE: 3
DONE: 4
}; };
$scope.state.ui = states.IDLE; // initial state $scope.state.ui = states.IDLE; // initial state
@ -37,9 +36,9 @@ define(function(require) {
}; };
/** /**
* Continue to set passphrase screen for keygen * Continue to keygen
*/ */
$scope.setPassphrase = function() { $scope.generateKey = function() {
if (!$scope.state.agree) { if (!$scope.state.agree) {
$scope.onError({ $scope.onError({
message: termsMsg message: termsMsg
@ -49,8 +48,29 @@ define(function(require) {
// sing up to newsletter // sing up to newsletter
$scope.signUpToNewsletter(); $scope.signUpToNewsletter();
// go to set passphrase screen // go to set keygen screen
$scope.setState(states.SET_PASSPHRASE); $scope.setState(states.PROCESSING);
setTimeout(function() {
emailDao.unlock({
passphrase: undefined // generate key without passphrase
}, function(err) {
if (err) {
$scope.setState(states.IDLE);
$scope.onError(err);
return;
}
appController._auth.storeCredentials(function(err) {
if (err) {
return $scope.onError(err);
}
$location.path('/desktop');
$scope.$apply();
});
});
}, 500);
}; };
/** /**
@ -87,86 +107,6 @@ define(function(require) {
xhr.send(formData); xhr.send(formData);
}; };
/*
* Taken from jQuery validate.password plug-in 1.0
* http://bassistance.de/jquery-plugins/jquery-plugin-validate.password/
*
* Copyright (c) 2009 Jörn Zaefferer
*
* Licensed under the MIT
* http://www.opensource.org/licenses/mit-license.php
*/
$scope.checkPassphraseQuality = function() {
var passphrase = $scope.state.passphrase;
$scope.passphraseRating = 0;
var LOWER = /[a-z]/,
UPPER = /[A-Z]/,
DIGIT = /[0-9]/,
DIGITS = /[0-9].*[0-9]/,
SPECIAL = /[^a-zA-Z0-9]/,
SAME = /^(.)\1+$/;
function uncapitalize(str) {
return str.substring(0, 1).toLowerCase() + str.substring(1);
}
if (!passphrase) {
// no rating for empty passphrase
$scope.passphraseMsg = '';
return;
}
if (passphrase.length < 8 || SAME.test(passphrase)) {
$scope.passphraseMsg = 'Very weak';
return;
}
var lower = LOWER.test(passphrase),
upper = UPPER.test(uncapitalize(passphrase)),
digit = DIGIT.test(passphrase),
digits = DIGITS.test(passphrase),
special = SPECIAL.test(passphrase);
if (lower && upper && digit || lower && digits || upper && digits || special) {
$scope.passphraseMsg = 'Strong';
$scope.passphraseRating = 3;
} else if (lower && upper || lower && digit || upper && digit) {
$scope.passphraseMsg = 'Good';
$scope.passphraseRating = 2;
} else {
$scope.passphraseMsg = 'Weak';
$scope.passphraseRating = 1;
}
};
$scope.confirmPassphrase = function() {
var passphrase = $scope.state.passphrase;
$scope.setState(states.PROCESSING);
setTimeout(function() {
emailDao.unlock({
passphrase: (passphrase) ? passphrase : undefined
}, function(err) {
if (err) {
$scope.setState(states.SET_PASSPHRASE);
$scope.onError(err);
return;
}
appController._auth.storeCredentials(function(err) {
if (err) {
return $scope.onError(err);
}
$location.path('/desktop');
$scope.$apply();
});
});
}, 500);
};
$scope.setState = function(state) { $scope.setState = function(state) {
$scope.state.ui = state; $scope.state.ui = state;
}; };

View File

@ -19,6 +19,7 @@ define(function(require) {
$scope.newPassphrase = undefined; $scope.newPassphrase = undefined;
$scope.oldPassphrase = undefined; $scope.oldPassphrase = undefined;
$scope.confirmation = undefined; $scope.confirmation = undefined;
$scope.passphraseMsg = undefined;
} }
}; };
@ -30,6 +31,59 @@ define(function(require) {
// scope functions // scope functions
// //
/*
* Taken from jQuery validate.password plug-in 1.0
* http://bassistance.de/jquery-plugins/jquery-plugin-validate.password/
*
* Copyright (c) 2009 Jörn Zaefferer
*
* Licensed under the MIT
* http://www.opensource.org/licenses/mit-license.php
*/
$scope.checkPassphraseQuality = function() {
var passphrase = $scope.newPassphrase;
$scope.passphraseRating = 0;
var LOWER = /[a-z]/,
UPPER = /[A-Z]/,
DIGIT = /[0-9]/,
DIGITS = /[0-9].*[0-9]/,
SPECIAL = /[^a-zA-Z0-9]/,
SAME = /^(.)\1+$/;
function uncapitalize(str) {
return str.substring(0, 1).toLowerCase() + str.substring(1);
}
if (!passphrase) {
// no rating for empty passphrase
$scope.passphraseMsg = '';
return;
}
if (passphrase.length < 8 || SAME.test(passphrase)) {
$scope.passphraseMsg = 'Very weak';
return;
}
var lower = LOWER.test(passphrase),
upper = UPPER.test(uncapitalize(passphrase)),
digit = DIGIT.test(passphrase),
digits = DIGITS.test(passphrase),
special = SPECIAL.test(passphrase);
if (lower && upper && digit || lower && digits || upper && digits || special) {
$scope.passphraseMsg = 'Strong';
$scope.passphraseRating = 3;
} else if (lower && upper || lower && digit || upper && digit) {
$scope.passphraseMsg = 'Good';
$scope.passphraseRating = 2;
} else {
$scope.passphraseMsg = 'Weak';
$scope.passphraseRating = 1;
}
};
$scope.setPassphrase = function() { $scope.setPassphrase = function() {
var keyId = pgp.getKeyParams()._id; var keyId = pgp.getKeyParams()._id;
keychain.lookupPrivateKey(keyId, function(err, savedKey) { keychain.lookupPrivateKey(keyId, function(err, savedKey) {

View File

@ -104,10 +104,6 @@
} }
} }
.passphrase-label-ok {
color: green;
}
.popover-info { .popover-info {
display: none; // hide on mobile display: none; // hide on mobile
} }

View File

@ -1,5 +1,14 @@
.view-set-passphrase { .view-set-passphrase {
p {
margin: 40px auto 0;
width: 90%;
}
.passphrase-label-ok {
color: green;
}
.inputs { .inputs {
margin: 40px 60px 30px; margin: 40px 60px 30px;
@ -9,17 +18,21 @@
} }
table { table {
margin: 50px auto 60px auto; margin: 40px auto 60px auto;
td { td {
padding-top: 15px; padding-top: 16px;
&:first-child { &:first-child {
text-align: right; text-align: right;
padding-right: 15px; padding-right: 16px;
font-weight: bold; font-weight: bold;
} }
} }
td.no-padding {
padding-top: 0;
}
} }
} }

View File

@ -21,28 +21,12 @@
<div> <div>
<button wo-touch="importKey()" class="btn btn-alt">Import existing key</button> <button wo-touch="importKey()" class="btn btn-alt">Import existing key</button>
<button type="submit" wo-touch="setPassphrase()" class="btn"tabindex="3">Generate new key</button> <button type="submit" wo-touch="generateKey()" class="btn"tabindex="3">Generate new key</button>
</div> </div>
</form> </form>
</div> </div>
<div ng-show="state.ui === 2"> <div ng-show="state.ui === 2">
<p><b>Set passphrase.</b> You can set a passphrase to protect your key on disk. This must be entered everytime you start the app. For no passphrase just press continue.</p>
<form>
<div>
<label class="input-error-message" ng-class="{'passphrase-label-ok': passphraseRating >= 2}">{{passphraseMsg}}</label><br>
<input class="input-text" type="password" ng-model="state.passphrase" ng-change="checkPassphraseQuality()" placeholder="Enter passphrase" tabindex="1" focus-me="true">
<input class="input-text" type="password" ng-model="state.confirmation" ng-class="{'input-text-error': (state.confirmation || state.passphrase) && state.confirmation !== state.passphrase}" placeholder="Confirm passphrase" tabindex="2">
</div>
<div>
<button type="submit" wo-touch="confirmPassphrase()" class="btn" ng-disabled="(state.confirmation || state.passphrase) && state.confirmation !== state.passphrase" tabindex="3">Continue</button>
</div>
</form>
</div>
<div ng-show="state.ui === 3">
<p><b>Generating key.</b> Please stand by. This can take a while...</p> <p><b>Generating key.</b> Please stand by. This can take a while...</p>
<div class="working"> <div class="working">
<span class="spinner"></span> <span class="spinner"></span>

View File

@ -6,16 +6,21 @@
<div class="content"> <div class="content">
<div class="dialog view-set-passphrase"> <div class="dialog view-set-passphrase">
<p>You can set a passphrase to protect your key on disk. This must be entered everytime you start the app.</p>
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td><label>Current passphrase</label></td> <td><label>Current passphrase</label></td>
<td><input class="input-text" type="password" ng-model="oldPassphrase" ng-change="checkPassphraseQuality()" tabindex="1" focus-me="true"></td> <td><input class="input-text" type="password" ng-model="oldPassphrase" tabindex="1" focus-me="true"></td>
</tr> </tr>
<tr> <tr>
<td><label>New passphrase</label></td> <td></td>
<td><input class="input-text" type="password" ng-model="newPassphrase" ng-change="checkPassphraseQuality()" tabindex="2"></td> <td><label class="input-error-message" ng-class="{'passphrase-label-ok': passphraseRating >= 2}">{{passphraseMsg}}</label></td>
</tr>
<tr>
<td class="no-padding"><label>New passphrase</label></td>
<td class="no-padding"><input class="input-text" type="password" ng-model="newPassphrase" ng-change="checkPassphraseQuality()" tabindex="2"></td>
</tr> </tr>
<tr> <tr>
<td><label>Confirm passphrase</label></td> <td><label>Confirm passphrase</label></td>

View File

@ -14,7 +14,6 @@ define(function(require) {
var scope, ctrl, location, origEmailDao, emailDaoMock, var scope, ctrl, location, origEmailDao, emailDaoMock,
origAuth, authMock, origAuth, authMock,
emailAddress = 'fred@foo.com', emailAddress = 'fred@foo.com',
passphrase = 'asd',
keyId, expectedKeyId, keyId, expectedKeyId,
cryptoMock; cryptoMock;
@ -57,7 +56,6 @@ define(function(require) {
describe('initial state', function() { describe('initial state', function() {
it('should be well defined', function() { it('should be well defined', function() {
expect(scope.confirmPassphrase).to.exist;
expect(scope.state.ui).to.equal(1); expect(scope.state.ui).to.equal(1);
}); });
}); });
@ -139,49 +137,7 @@ define(function(require) {
}); });
}); });
describe('check passphrase quality', function() { describe('generate key', function() {
it('should be too short', function() {
scope.state.passphrase = '&§DG36';
scope.checkPassphraseQuality();
expect(scope.passphraseMsg).to.equal('Very weak');
expect(scope.passphraseRating).to.equal(0);
});
it('should be very weak', function() {
scope.state.passphrase = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
scope.checkPassphraseQuality();
expect(scope.passphraseMsg).to.equal('Very weak');
expect(scope.passphraseRating).to.equal(0);
});
it('should be weak', function() {
scope.state.passphrase = 'asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf';
scope.checkPassphraseQuality();
expect(scope.passphraseMsg).to.equal('Weak');
expect(scope.passphraseRating).to.equal(1);
});
it('should be good', function() {
scope.state.passphrase = 'asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf5';
scope.checkPassphraseQuality();
expect(scope.passphraseMsg).to.equal('Good');
expect(scope.passphraseRating).to.equal(2);
});
it('should be strong', function() {
scope.state.passphrase = '&§DG36abcd';
scope.checkPassphraseQuality();
expect(scope.passphraseMsg).to.equal('Strong');
expect(scope.passphraseRating).to.equal(3);
});
});
describe('setPassphrase', function() {
var signUpToNewsletterStub; var signUpToNewsletterStub;
beforeEach(function() { beforeEach(function() {
signUpToNewsletterStub = sinon.stub(scope, 'signUpToNewsletter'); signUpToNewsletterStub = sinon.stub(scope, 'signUpToNewsletter');
@ -195,79 +151,49 @@ define(function(require) {
scope.onError = function(err) { scope.onError = function(err) {
expect(err.message).to.contain('Terms'); expect(err.message).to.contain('Terms');
expect(scope.state.ui).to.equal(1);
expect(signUpToNewsletterStub.called).to.be.false; expect(signUpToNewsletterStub.called).to.be.false;
done(); done();
}; };
scope.setPassphrase(); scope.generateKey();
}); });
it('should continue', function(done) { it('should fail due to error in emailDao.unlock', function(done) {
scope.state.agree = true; scope.state.agree = true;
var setStateStub = sinon.stub(scope, 'setState', function(state) { emailDaoMock.unlock.withArgs({
expect(setStateStub.calledOnce).to.be.true; passphrase: undefined
expect(signUpToNewsletterStub.calledOnce).to.be.true; }).yields(new Error());
expect(state).to.equal(2); authMock.storeCredentials.yields();
scope.onError = function(err) {
expect(err).to.exist;
expect(scope.state.ui).to.equal(1);
expect(signUpToNewsletterStub.called).to.be.true;
done(); done();
}); };
scope.setPassphrase(); scope.generateKey();
expect(scope.state.ui).to.equal(2);
}); });
});
describe('confirm passphrase', function() {
var setStateStub;
it('should unlock crypto', function(done) { it('should unlock crypto', function(done) {
scope.state.passphrase = passphrase; scope.state.agree = true;
scope.state.confirmation = passphrase;
emailDaoMock.unlock.withArgs({ emailDaoMock.unlock.withArgs({
passphrase: passphrase passphrase: undefined
}).yields(); }).yields();
authMock.storeCredentials.yields(); authMock.storeCredentials.yields();
scope.$apply = function() { scope.$apply = function() {
expect(scope.state.ui).to.equal(2);
expect(location.$$path).to.equal('/desktop'); expect(location.$$path).to.equal('/desktop');
expect(emailDaoMock.unlock.calledOnce).to.be.true; expect(emailDaoMock.unlock.calledOnce).to.be.true;
done(); done();
}; };
scope.confirmPassphrase(); scope.generateKey();
});
it('should not do anything matching passphrases', function() {
scope.state.passphrase = 'a';
scope.state.confirmation = 'b';
scope.confirmPassphrase();
});
it('should not work when keypair generation fails', function(done) {
scope.state.passphrase = passphrase;
scope.state.confirmation = passphrase;
emailDaoMock.unlock.withArgs({
passphrase: passphrase
}).yields(new Error('asd'));
setStateStub = sinon.stub(scope, 'setState', function(state) {
if (setStateStub.calledOnce) {
expect(state).to.equal(3);
} else if (setStateStub.calledTwice) {
expect(state).to.equal(2);
expect(emailDaoMock.unlock.calledOnce).to.be.true;
scope.setState.restore();
}
});
scope.onError = function(err) {
expect(err.message).to.equal('asd');
done();
};
scope.confirmPassphrase();
}); });
}); });

View File

@ -79,5 +79,48 @@ define(function(require) {
scope.setPassphrase(); scope.setPassphrase();
}); });
}); });
describe('check passphrase quality', function() {
it('should be too short', function() {
scope.newPassphrase = '&§DG36';
scope.checkPassphraseQuality();
expect(scope.passphraseMsg).to.equal('Very weak');
expect(scope.passphraseRating).to.equal(0);
});
it('should be very weak', function() {
scope.newPassphrase = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
scope.checkPassphraseQuality();
expect(scope.passphraseMsg).to.equal('Very weak');
expect(scope.passphraseRating).to.equal(0);
});
it('should be weak', function() {
scope.newPassphrase = 'asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf';
scope.checkPassphraseQuality();
expect(scope.passphraseMsg).to.equal('Weak');
expect(scope.passphraseRating).to.equal(1);
});
it('should be good', function() {
scope.newPassphrase = 'asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf5';
scope.checkPassphraseQuality();
expect(scope.passphraseMsg).to.equal('Good');
expect(scope.passphraseRating).to.equal(2);
});
it('should be strong', function() {
scope.newPassphrase = '&§DG36abcd';
scope.checkPassphraseQuality();
expect(scope.passphraseMsg).to.equal('Strong');
expect(scope.passphraseRating).to.equal(3);
});
});
}); });
}); });