Implemented PIN and updated README

master-GE
Dario Chiappetta 2015-09-30 20:09:58 +02:00
parent 4710a571e7
commit 3dcdcbbe7e
15 changed files with 588 additions and 138 deletions

View File

@ -4,32 +4,32 @@
##What does Clipperz do?
Clipperz is an online vault where you can store confidential data without worrying about security. It can be used to save and manage passwords, private notes, burglar alarm codes, credit and debit card details, PINs, software keys, …
Since passwords are the most common type of private information that you need to protect, we have added a great deal of functionality to make Clipperz a great [online password manager][home] thus solving the “password fatigue” problem.
Clipperz is a smart online vault where you can store confidential data without worrying about security. It can be used to save and manage passwords, private notes, burglar alarm codes, credit and debit card details, PINs, software keys, …
Since passwords are the most common type of private information that you need to protect, we have added a great deal of functionality to make Clipperz a great [online password manager][home]. Read more on the [Clipperz website][home].
**Clipperz makes the Internet the most convenient and safe place to keep you most precious and sensitive data.**
Read more on the [Clipperz website][home].
[home]: https://clipperz.is
## Why an open source version of Clipperz?
Because we want to enable as many people as possible to play with our code. So that they can start trusting it. The code, not its developers.
In order to allow anyone not just to inspect the source code, but also to analyze the traffic it generates between client and server, we made available this open source version as an easy way to locally deploy the whole password manager web app on your machine. You can choose among the available backends (PHP/MySQL, Python/AppEngine, …) or [contribute][CA] your own.
Because we want to enable as many people as possible to play with the very same code that is powering [Clipperz online service][app]. The goal is building trust. Trust in the code, not in its developers!
So we released the frontend code under an open source license. That was not enough. In order to allow anyone not just to inspect the code running in the user browser, but also to analyze the traffic it generates between the client (the user's browser) and the Clipperz server, we also made available several backends that are easy to deploy.
You can choose among the available backends (PHP/MySQL, Python/AppEngine, …) or [contribute][CA] your own.
Whatever is your motivation for playing with Clipperz code, we would love to hear from you: [get in contact][contact]!
## Security warning
# Security warning
The open source version of Clipperz is suitable for **testing and educational purposes only**. Do not use it as an actual password management solution.
The open source version of Clipperz is suitable for **testing and educational purposes only**.
As an example, the current PHP backend lacks several critical capabilities such as bot protection and concurrent sessions management, moreover it could be vulnerable to serious threats (SQL injections, remote code execution, ...).
[CA]: https://clipperz.is/open_source/contributor_agreement
[contact]: https://clipperz.is/about/contacts
[clipperz]: https://clipperz.is
Please note: the actual Clipperz service use a far more robust backend, but the communication protocol between backend and frontend is of course identical.
[app]: https://clipperz.is/app/
[CA]: /open_source/contributor_agreement/
[contact]: /about/contacts/
## Donations
@ -64,8 +64,8 @@ In order to build the deployable version, you need to invoke the following comma
git clone git@github.com:clipperz/password-manager.git
cd password-manager
./scripts/build install --backends php python --frontends beta gamma
./scripts/build install --backends php python --frontends delta
The output will be available in the `target` folder, with a separate folder for each backend (currently the available options are `php` and `python`).
The script, invoked with these parameters, will build both the full version (`install` -> index.html) and the debug version (index_debug.html) of the specified frontends.
@ -87,14 +87,12 @@ The only file that needs to be `build`, and not read directly from the file syst
In order to build this file, the following command should be executed:
./scripts/build --frontends beta gamma gamma.mobile --backends dev
./scripts/build --frontends delta --backends dev
Once the index.html files have been built (one for each frontend) and a backend is running and has been correctly configured in the proxy script, it is possible to access the different versions of the application at the following URLs:
Once the index.html file has been built and a backend is running and has been correctly configured in the proxy script, it is possible to access the application at the following URL:
- `http://localhost:8888/beta/index.html`
- `http://localhost:8888/gamma/index.html`
- `http://localhost:8888/gamma/index.mobile.html`
- `http://localhost:8888/delta/index.html`
## Installing

View File

@ -119,7 +119,6 @@ http://jonibologna.com/flexbox-cheatsheet/
-ms-transform: rotate(0deg) translate(0, 0);
-o-transform: rotate(0deg) translate(0, 0);
transform: rotate(0deg) translate(0, 0); }
100% {
-webkit-transform: rotate(359deg) translate(0, 0);
-moz-transform: rotate(359deg) translate(0, 0);
@ -133,7 +132,6 @@ http://jonibologna.com/flexbox-cheatsheet/
-ms-transform: rotate(0deg) translate(0, 0);
-o-transform: rotate(0deg) translate(0, 0);
transform: rotate(0deg) translate(0, 0); }
100% {
-webkit-transform: rotate(359deg) translate(0, 0);
-moz-transform: rotate(359deg) translate(0, 0);
@ -147,7 +145,6 @@ http://jonibologna.com/flexbox-cheatsheet/
-ms-transform: rotate(0deg) translate(0, 0);
-o-transform: rotate(0deg) translate(0, 0);
transform: rotate(0deg) translate(0, 0); }
100% {
-webkit-transform: rotate(359deg) translate(0, 0);
-moz-transform: rotate(359deg) translate(0, 0);
@ -161,7 +158,6 @@ http://jonibologna.com/flexbox-cheatsheet/
-ms-transform: rotate(0deg) translate(0, 0);
-o-transform: rotate(0deg) translate(0, 0);
transform: rotate(0deg) translate(0, 0); }
100% {
-webkit-transform: rotate(359deg) translate(0, 0);
-moz-transform: rotate(359deg) translate(0, 0);
@ -480,73 +476,61 @@ div.overlay {
@-webkit-keyframes overlay-spin {
from {
opacity: 1; }
to {
opacity: 0.25; } }
@-moz-keyframes overlay-spin {
from {
opacity: 1; }
to {
opacity: 0.25; } }
@-ms-keyframes overlay-spin {
from {
opacity: 1; }
to {
opacity: 0.25; } }
@keyframes overlay-spin {
from {
opacity: 1; }
to {
opacity: 0.25; } }
@-webkit-keyframes ios-overlay-show {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@-moz-keyframes ios-overlay-show {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@-ms-keyframes ios-overlay-show {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@keyframes ios-overlay-show {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@-webkit-keyframes ios-overlay-hide {
0% {
opacity: 1; }
100% {
opacity: 0; } }
@-moz-keyframes ios-overlay-hide {
0% {
opacity: 1; }
100% {
opacity: 0; } }
@-ms-keyframes ios-overlay-hide {
0% {
opacity: 1; }
100% {
opacity: 0; } }
@keyframes ios-overlay-hide {
0% {
opacity: 1; }
100% {
opacity: 0; } }
/*
@ -1755,6 +1739,37 @@ div.help {
font-weight: 100;
background-color: #c0c0c0;
cursor: default; }
#loginPage .content .body .pinForm, #registrationPage .content .body .pinForm, #unlockPage .content .body .pinForm {
/*
.pinContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
.pinDigit {
width: 15%;
}
}
*/ }
#loginPage .content .body .pinForm label, #registrationPage .content .body .pinForm label, #unlockPage .content .body .pinForm label {
display: inherit;
text-align: left; }
#loginPage .content .body .pinForm .pinValue, #registrationPage .content .body .pinForm .pinValue, #unlockPage .content .body .pinForm .pinValue {
font-family: clipperz-password; }
#loginPage .content .body .pinForm .pinValue::-webkit-input-placeholder, #registrationPage .content .body .pinForm .pinValue::-webkit-input-placeholder, #unlockPage .content .body .pinForm .pinValue::-webkit-input-placeholder {
font-family: clipperz-font; }
#loginPage .content .body .pinForm .pinValue:-moz-placeholder, #registrationPage .content .body .pinForm .pinValue:-moz-placeholder, #unlockPage .content .body .pinForm .pinValue:-moz-placeholder {
font-family: clipperz-font; }
#loginPage .content .body .pinForm .pinValue::-moz-placeholder, #registrationPage .content .body .pinForm .pinValue::-moz-placeholder, #unlockPage .content .body .pinForm .pinValue::-moz-placeholder {
font-family: clipperz-font; }
#loginPage .content .body .pinForm .pinValue:-ms-input-placeholder, #registrationPage .content .body .pinForm .pinValue:-ms-input-placeholder, #unlockPage .content .body .pinForm .pinValue:-ms-input-placeholder {
font-family: clipperz-font; }
#loginPage .content .body .pinForm .passphraseLogin, #registrationPage .content .body .pinForm .passphraseLogin, #unlockPage .content .body .pinForm .passphraseLogin {
font-size: .9em;
color: #ff9900;
text-decoration: underline;
cursor: pointer; }
#loginPage .content .other, #registrationPage .content .other, #unlockPage .content .other {
font-size: 24pt;
padding: 20px;
@ -2206,13 +2221,13 @@ span.count {
#extraFeaturesPanel .extraFeatureIndex footer {
font-size: 8pt;
padding: 5px 5px 5px 5px;
border-top: 1px solid #999999; }
border-top: 1px solid #999; }
#extraFeaturesPanel .extraFeatureIndex footer span {
color: #999999; }
color: #999; }
#extraFeaturesPanel .extraFeatureIndex footer span:after {
content: ":"; }
#extraFeaturesPanel .extraFeatureIndex footer a {
color: #999999;
color: #999;
text-decoration: none;
padding-left: 5px;
font-weight: bold; }
@ -2225,6 +2240,8 @@ span.count {
font-size: 20pt; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .header p {
padding: 10px 0px; }
#extraFeaturesPanel .extraFeatureContent .extraFeature .header strong {
font-weight: bold; }
#extraFeaturesPanel .extraFeatureContent .extraFeature form label {
display: none; }
#extraFeaturesPanel .extraFeatureContent .extraFeature form input {
@ -2585,6 +2602,17 @@ span.count {
padding: 0 1em;
text-decoration: underline;
cursor: pointer; }
#extraFeaturesPanel .extraFeatureContent .devicePIN .pinDigit {
display: inline-block;
width: 2em;
margin-right: 0.5em;
text-align: center; }
#extraFeaturesPanel .extraFeatureContent .devicePIN .pinValue {
display: inline-block;
width: 4em;
margin-right: 1em; }
#extraFeaturesPanel .extraFeatureContent .devicePIN :enabled {
border: 2px solid #ff9900; }
#extraFeaturesPanel .extraFeatureContent .dataImport .content {
display: block;
height: 100%;
@ -2994,7 +3022,7 @@ div.cardList ul {
padding-right: 0px;
box-shadow: -4px 0px 3px -1px rgba(0, 0, 0, 0.2); }
div.cardList ul li.archived {
background-color: #eeeeee;
background-color: #eee;
color: #999; }
div.cardList ul li .favicon {
width: 48px;
@ -3081,7 +3109,7 @@ div.cardList.narrow {
content: ""; }
#cardDetailPage .view.archived, .cardDetail .view.archived {
background-color: #eeeeee; }
background-color: #eee; }
#cardDetailPage .view .cardDetailToolbar, .cardDetail .view .cardDetailToolbar {
background-color: #1863a1;
color: white; }
@ -3311,7 +3339,7 @@ div.cardList.narrow {
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
background: repeating-linear-gradient(0deg, white, white 2px, #dddddd 2px, #dddddd 3px);
background: repeating-linear-gradient(0deg, white, white 2px, #ddd 2px, #ddd 3px);
width: 28px;
height: 20px;
margin-left: 6px;
@ -3537,7 +3565,7 @@ div.cardList.narrow {
min-width: 220px;
width: 80%;
max-width: 400px;
background-color: #333333;
background-color: #333;
color: #fff;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
@ -3552,7 +3580,7 @@ div.cardList.narrow {
margin-left: 0px;
width: 0;
height: 0;
border-top: 5px solid #333333;
border-top: 5px solid #333;
border-left: 5px solid transparent;
border-right: 5px solid transparent; }
.passwordGenerator .passwordGeneratorBaloon form span {

File diff suppressed because one or more lines are too long

View File

@ -39,18 +39,18 @@ MochiKit.Base.update(Clipperz.PM.PIN, {
return this.__repr__();
},
'CREDENTIALS': 'CLIPPERZ.CREDENTIALS',
'FAILURE_COUNT': 'CLIPPERZ.FAILED_LOGIN_COUNT',
'ENCRYPTED_PASSPHRASE_LENGTH': 1024,
'DEFAULT_PIN_LENGTH': 5,
'ALLOWED_RETRY': 3,
'LS_USERNAME': 'clipperz.pin.username',
'LS_PASSPHRASE': 'clipperz.pin.passphrase',
'LS_FAILURE_COUNT': 'clipperz.pin.failureCount',
//-------------------------------------------------------------------------
'isSet': function () {
return (this.storedCredentials() != null);
},
'storedCredentials': function () {
return localStorage[this.CREDENTIALS];
return (localStorage[this.LS_USERNAME] && localStorage[this.LS_PASSPHRASE]);
},
//-------------------------------------------------------------------------
@ -59,7 +59,7 @@ MochiKit.Base.update(Clipperz.PM.PIN, {
var failureCount;
var result;
failureCount = localStorage[this.FAILURE_COUNT];
failureCount = localStorage[this.LS_FAILURE_COUNT];
if (failureCount == null) {
failureCount = 0
@ -68,10 +68,10 @@ MochiKit.Base.update(Clipperz.PM.PIN, {
failureCount ++;
if (failureCount < this.ALLOWED_RETRY) {
localStorage[this.FAILURE_COUNT] = failureCount;
localStorage[this.LS_FAILURE_COUNT] = failureCount;
result = failureCount;
} else {
this.removeLocalCredentials();
this.disablePin();
result = -1;
}
@ -79,11 +79,11 @@ MochiKit.Base.update(Clipperz.PM.PIN, {
},
'resetFailedAttemptCount': function () {
localStorage.removeItem(this.FAILURE_COUNT);
localStorage.removeItem(this.LS_FAILURE_COUNT);
},
'failureCount': function () {
return localStorage[this.FAILURE_COUNT];
return localStorage[this.LS_FAILURE_COUNT];
},
//-------------------------------------------------------------------------
@ -92,37 +92,51 @@ MochiKit.Base.update(Clipperz.PM.PIN, {
return Clipperz.Crypto.SHA.sha256(new Clipperz.ByteArray(aPIN));
},
'credentialsWithPIN': function (aPIN) {
var byteArrayValue;
var decryptedValue;
var result;
byteArrayValue = (new Clipperz.ByteArray()).appendBase64String(localStorage[this.CREDENTIALS]);
decryptedValue = Clipperz.Crypto.AES.decrypt(this.deriveKeyFromPin(aPIN), byteArrayValue).asString();
try {
result = Clipperz.Base.evalJSON(decryptedValue);
} catch (error) {
result = {'username':'fakeusername', 'passphrase':'fakepassphrase'};
'credentialsWithPIN': function(aPIN) {
return {
'username': localStorage[this.LS_USERNAME],
'passphrase': this.decryptPassphraseWithPin(aPIN, localStorage[this.LS_PASSPHRASE]),
}
return result;
},
'setCredentialsWithPIN': function (aPIN, someCredentials) {
var encodedValue;
var byteArrayValue;
var encryptedValue;
'encryptPassphraseWithPin': function(aPIN, aPassphrase) {
var byteArrayPassphrase = new Clipperz.ByteArray(aPassphrase);
// var hashedPassphrase = Clipperz.Crypto.SHA.sha_d256(ba) // ??? why would i hash the passphrase???
var randomBytesLength = this.ENCRYPTED_PASSPHRASE_LENGTH-byteArrayPassphrase.length()-1;
var randomBytes = Clipperz.Crypto.PRNG.defaultRandomGenerator().getRandomBytes(randomBytesLength);
var derivedKey = this.deriveKeyFromPin(aPIN);
encodedValue = Clipperz.Base.serializeJSON(someCredentials);
byteArrayValue = new Clipperz.ByteArray(encodedValue);
encryptedValue = Clipperz.Crypto.AES.encrypt(this.deriveKeyFromPin(aPIN), byteArrayValue).toBase64String();
byteArrayPassphrase.appendByte(0);
byteArrayPassphrase.appendBytes(randomBytes.arrayValues());
localStorage[this.CREDENTIALS] = encryptedValue;
return Clipperz.Crypto.AES.encrypt(derivedKey, byteArrayPassphrase).toBase64String();
},
'removeLocalCredentials': function () {
localStorage.removeItem(this.CREDENTIALS);
localStorage.removeItem(this.FAILURE_COUNT);
'decryptPassphraseWithPin': function(aPIN, anEncryptedPassphrase) {
var byteArrayEncryptedPassphrase = (new Clipperz.ByteArray()).appendBase64String(anEncryptedPassphrase);
var derivedKey = this.deriveKeyFromPin(aPIN);
var byteArrayPassphrase = Clipperz.Crypto.AES.decrypt(derivedKey, byteArrayEncryptedPassphrase);
var arrayPassphrase = byteArrayPassphrase.arrayValues();
var slicedArrayPassphrase = arrayPassphrase.slice(0, arrayPassphrase.indexOf(0));
return new Clipperz.ByteArray(slicedArrayPassphrase).asString();
},
'updatePin': function(aUser, aPIN) {
return Clipperz.Async.callbacks("Clipperz.PM.PIN", [
MochiKit.Base.method(aUser, 'username'),
MochiKit.Base.method(localStorage, 'setItem', this.LS_USERNAME),
MochiKit.Base.method(aUser, 'getPassphrase'),
MochiKit.Base.method(this, 'encryptPassphraseWithPin', aPIN),
MochiKit.Base.method(localStorage, 'setItem', this.LS_PASSPHRASE),
MochiKit.Base.method(localStorage, 'setItem', this.LS_FAILURE_COUNT, 0),
], {trace:false});
},
'disablePin': function () {
localStorage.removeItem(this.LS_USERNAME);
localStorage.removeItem(this.LS_PASSPHRASE);
localStorage.removeItem(this.LS_FAILURE_COUNT);
},
'isLocalStorageSupported': function() {

View File

@ -403,7 +403,7 @@ Clipperz.Base.extend(Clipperz.PM.Proxy.Offline.DataStore, Object, {
);
result['M2'] = M2;
result['accountInfo'] = aConnection['userData']['accountInfo'];
result['lock'] = '<<LOCK>>';
result['lock'] = (aConnection['userData']['lock']) ? aConnection['userData']['lock'] : '<<LOCK>>';
} else {
throw new Error("Client checksum verification failed! Expected <" + M1 + ">, received <" + someParameters.parameters.M1 + ">.", "Error");
}

View File

@ -31,15 +31,180 @@ Clipperz.PM.UI.Components.ExtraFeatures.DevicePINClass = React.createClass({
// 'level': React.PropTypes.oneOf(['hide', 'info', 'warning', 'error']).isRequired
},
getInitialState: function() {
return {
isEditing: false,
pinValue: ''
}
},
_editModeLocked: false,
//=========================================================================
enterEditMode: function() {
this.setState({
'isEditing': true,
'pinValue': ''
});
},
exitEditMode: function() {
this.setState({
'isEditing': false,
});
},
lockEditMode: function() {
this._editModeLocked = true;
},
unlockEditMode: function() {
this._editModeLocked = false;
},
handleFocus: function(anEvent) {
anEvent.preventDefault();
this.refs['pinValue'].getDOMNode().focus();
},
handleBlur: function(anEvent) {
if (! this._editModeLocked) {
if (anEvent.target.value.length < this.props['PIN'].DEFAULT_PIN_LENGTH) {
this.exitEditMode();
}
}
},
handleKeyDown: function(anEvent) {
if (anEvent.keyCode == 27) {
this.refs['pinValue'].getDOMNode().blur();
}
},
handleChange: function(anEvent) {
if (anEvent.target.value.length == this.props['PIN'].DEFAULT_PIN_LENGTH) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'updatePIN', anEvent.target.value);
this.refs['pinValue'].getDOMNode().blur();
this.exitEditMode();
} else {
this.setState({
'pinValue': anEvent.target.value
});
}
},
handleCheckboxChange: function(anEvent) {
if (this.props['PIN'].isSet() || this.state['isEditing']) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'disablePIN', anEvent.target.value);
this.exitEditMode();
} else {
this.enterEditMode();
}
},
handleResetPIN: function() {
this.enterEditMode();
},
//=========================================================================
// renderDigitInputs: function() {
// var i;
// var result;
// result = [];
// for (i = 0; i<this.props['PIN'].DEFAULT_PIN_LENGTH; i++) {
// var boxIsFull = (this.state['isEditing']&&this.state['pinValue'][i])
// ||
// (!this.state['isEditing']&&this.props['PIN'].isSet())
// result.push(React.DOM.input({
// 'key': 'pin-digit-'+i,
// 'ref': 'pin-digit-'+i,
// 'name': 'pin-digit-'+i,
// 'className': 'pinDigit',
// 'readOnly': true,
// 'type': 'text',
// 'value': boxIsFull ? '*' : '',
// 'min': 0,
// 'max': 9,
// 'disabled': !this.state['isEditing'],
// 'onFocus': this.handleFocus,
// }));
// }
// return result;
// },
//-------------------------------------------------------------------------
componentDidUpdate: function() {
if (this.state['isEditing']) {
this.refs['pinValue'].getDOMNode().focus();
}
},
render: function () {
return React.DOM.div({className:'extraFeature devicePIN'}, [
var displayedPin;
var isFormEnabled = (this.props['PIN'].isSet() || this.state.isEditing);
var isResetButtonEnabled = (! this.state['isEditing'] && this.props['PIN'].isSet());
if (this.state.isEditing) {
displayedPin = this.state['pinValue'];
} else {
displayedPin = (this.props['PIN'].isSet()) ? '*****' : '';
}
return React.DOM.div({className:'extraFeature devicePIN'}, [
React.DOM.div({'className':'header'}, [
React.DOM.h1({}, "Device PIN"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "You may create a 5-digit PIN to be used instead of your passphrase. Please note that the PIN is specific to the device you are now using."),
React.DOM.p({}, [
React.DOM.strong({}, "Warning"),
": enabling a PIN on your device may represent a security risk! Make sure to keep the device with you at all times!",
]),
]),
]),
React.DOM.div({'className': 'content'}, [
React.DOM.h3({}, this.props['PIN'])
React.DOM.form({},[
React.DOM.p({}, [
React.DOM.input({
'type': 'checkbox',
'key': 'pinEnabled',
'checked': isFormEnabled,
'onChange': this.handleCheckboxChange,
'onMouseDown': this.lockEditMode,
'onMouseUp': this.unlockEditMode,
}),
React.DOM.label({
'key': 'pinEnabledLabel',
'htmlFor': 'pinEnabled',
'onClick': this.handleCheckboxChange,
'onMouseDown': this.lockEditMode,
'onMouseUp': this.unlockEditMode,
}, "Enable PIN on your device")
]),
// this.renderDigitInputs(),
React.DOM.input({
'type': 'tel',
'key': 'pinValue',
'ref': 'pinValue',
'className': 'pinValue',
'disabled': !this.state['isEditing'],
'onKeyDown': this.handleKeyDown,
'onChange': this.handleChange,
'onBlur': this.handleBlur,
'value': displayedPin,
// 'style': {'position': 'fixed', 'top': -1000}
}),
React.DOM.a({
'className': 'button'+(isResetButtonEnabled ? '' : ' disabled'),
'onClick': (isResetButtonEnabled) ? this.handleResetPIN : null
}, "Reset PIN"),
])
])
]);
},

View File

@ -71,7 +71,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.PreferencesClass = React.createClass({
return MochiKit.Base.bind(function (anEvent) {
var value = anEvent.target.value;
console.log("HANDLE KEY DOWN", anEvent, anEvent.keyCode, value);
// console.log("HANDLE KEY DOWN", anEvent, anEvent.keyCode, value);
if (anEvent.target.defaultValue != value) {
switch (anEvent.keyCode) {
case 9: // tab
@ -80,7 +80,7 @@ console.log("HANDLE KEY DOWN", anEvent, anEvent.keyCode, value);
anEvent.target.defaultValue = anEvent.target.value;
break;
case 27: // escape
console.log("ESCAPE");
// console.log("ESCAPE");
anEvent.target.value = anEvent.target.defaultValue;
break;
}

View File

@ -45,12 +45,18 @@ Clipperz.PM.UI.Components.Pages.LoginPageClass = React.createClass({
return {
username: '',
passphrase: '',
pin: ''
pin: '',
};
},
//=========================================================================
mode: function() {
return (this.props['mode'] == 'CREDENTIALS' || this.props['forceCredentials']) ? 'CREDENTIALS' : 'PIN';
},
//=========================================================================
handleChange: function (anEvent) {
var refs = this.refs;
var refName = MochiKit.Base.filter(function (aRefName) { return refs[aRefName].getDOMNode() == anEvent.target}, MochiKit.Base.keys(this.refs))[0];
@ -61,7 +67,7 @@ Clipperz.PM.UI.Components.Pages.LoginPageClass = React.createClass({
},
pollForChanges: function() {
if (this.props.mode == 'CREDENTIALS') {
if (this.mode() == 'CREDENTIALS') {
var newState;
var usernameValue = this.refs['username'].getDOMNode().value;
@ -102,8 +108,8 @@ Clipperz.PM.UI.Components.Pages.LoginPageClass = React.createClass({
return (
((this.state['username'] != '') && (this.state['passphrase'] != ''))
||
(this.state['pin'] != '')
// ||
// (this.state['pin'] != '')
)
&&
!this.props['disabled'];
@ -121,9 +127,7 @@ Clipperz.PM.UI.Components.Pages.LoginPageClass = React.createClass({
]);
},
handlePINSubmit: function (event) {
event.preventDefault();
submitPIN: function (event) {
this.refs['pin'].getDOMNode().blur();
var credentials = {
@ -133,19 +137,82 @@ Clipperz.PM.UI.Components.Pages.LoginPageClass = React.createClass({
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'doLogin', credentials);
},
forcePassphraseLogin: function() {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'forcePassphraseLogin');
},
handlePinChange: function(anEvent) {
if (anEvent.target.value.length == Clipperz.PM.PIN.DEFAULT_PIN_LENGTH) {
this.submitPIN();
}
this.setState({
'pin': anEvent.target.value
})
},
// handlePinFocus: function(anEvent) {
// // anEvent.preventDefault();
// this.refs['pin'].getDOMNode().focus();
// },
// pinFormDigits: function() {
// var i;
// var result;
// result = [];
// for (i = 0; i<Clipperz.PM.PIN.DEFAULT_PIN_LENGTH; i++) {
// result.push(React.DOM.input({
// 'key': 'pin-digit-'+i,
// 'ref': 'pin-digit-'+i,
// 'name': 'pin-digit-'+i,
// 'className': 'pinDigit',
// 'readOnly': true,
// 'type': 'text',
// 'value': this.state['pin'][i],
// 'onFocus': this.handlePinFocus,
// }));
// }
// return result;
// },
pinForm: function () {
return React.DOM.form({'className':'pinForm pin', 'autoComplete':'off', 'onChange':this.handleChange, 'onSubmit':this.handlePINSubmit}, [
React.DOM.div({'key':'pinFormDiv'},[
React.DOM.label({'for':'pin'}, "pin"),
React.DOM.input({'type':'text', 'name':'pin', 'ref':'pin', placeholder:"PIN", 'key':'pin', 'autocapitalize':'none'})
]),
React.DOM.button({'key':'submitButton', 'type':'submit', 'disabled':this.props.disabled, 'className':'button'}, "login")
]);
return React.DOM.form({
'className':'pinForm pin',
'autoComplete':'off',
'onSubmit': function(anEvent) {anEvent.preventDefault();},
}, [
React.DOM.div({'key':'pinFormDiv'},[
React.DOM.label({'htmlFor':'pin'}, "Enter your PIN"),
React.DOM.input({
'type':'tel',
'name':'pin',
'ref':'pin',
'id': 'pinValue',
'className': 'pinValue',
'placeholder':"PIN",
'key':'pin',
'autoCapitalize':'none',
'value': this.state['pin'],
'onChange': this.handlePinChange,
}),
// React.DOM.div({'className': 'pinContainer'}, this.pinFormDigits()),
React.DOM.a({
'className': 'passphraseLogin',
'onClick': this.forcePassphraseLogin,
}, "Login with passphrase")
]),
// React.DOM.button({'key':'submitButton', 'type':'submit', 'disabled':this.props.disabled, 'className':'button'}, "login")
]);
},
setInitialFocus: function () {
if (this.props.mode == 'PIN') {
this.refs['pin'].getDOMNode().select();
if (this.mode() == 'PIN') {
this.setState({
'pin': ''
})
this.refs['pin'].getDOMNode().focus();
} else {
if (this.refs['username'].getDOMNode().value == '') {
this.refs['username'].getDOMNode().focus();
@ -162,7 +229,6 @@ Clipperz.PM.UI.Components.Pages.LoginPageClass = React.createClass({
},
render: function() {
//console.log("LOGIN PAGE", this.props);
// var registrationLink = React.DOM.footer({'key':'registrationLink', 'className':'registrationLink'}, [
// React.DOM.a({'key':'signup', 'onClick':this.handleRegistrationLinkClick}, "Sign up")
// ]);
@ -183,7 +249,7 @@ Clipperz.PM.UI.Components.Pages.LoginPageClass = React.createClass({
]),
React.DOM.div({'key':'formWrapper', 'className':'form body'}, [
React.DOM.div({'className':'bodyContent'}, [
this.props['mode'] == 'PIN' ? this.pinForm() : this.loginForm(),
this.mode() == 'PIN' ? this.pinForm() : this.loginForm(),
]),
]),
this.props['isNewUserRegistrationAvailable'] ? registrationLink : null,

View File

@ -41,6 +41,12 @@ Clipperz.PM.UI.Components.Pages.UnlockPageClass = React.createClass({
//=========================================================================
mode: function() {
return (this.props['mode'] == 'CREDENTIALS' || this.props['forceCredentials']) ? 'CREDENTIALS' : 'PIN';
},
//=========================================================================
handleChange: function (anEvent) {
var newState = {};
@ -49,27 +55,58 @@ Clipperz.PM.UI.Components.Pages.UnlockPageClass = React.createClass({
this.setState(newState);
},
handlePinChange: function(anEvent) {
if (anEvent.target.value.length == this.props['PIN'].DEFAULT_PIN_LENGTH) {
this.submitPIN();
}
this.setState({
'pin': anEvent.target.value
})
},
//=========================================================================
handlePassphraseSubmit: function (event) {
event.preventDefault();
this.refs['passphrase'].getDOMNode().blur();
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'unlock', this.refs['passphrase'].getDOMNode().value);
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'unlock', this.refs['passphrase'].getDOMNode().value, 'PASSPHRASE');
this.resetUnlockForm();
},
submitPIN: function() {
this.refs['pin'].getDOMNode().blur();
var pin = this.refs['pin'].getDOMNode().value;
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'unlock', pin, 'PIN');
this.resetUnlockForm();
},
resetUnlockForm: function() {
this.refs['passphrase'].getDOMNode().value = '';
this.replaceState(this.getInitialState());
if (this.mode() == 'CREDENTIALS') {
this.refs['passphrase'].getDOMNode().value = '';
this.refs['passphrase'].getDOMNode().blur();
} else if (this.mode() == 'PIN') {
this.refs['pin'].getDOMNode().value = '';
this.refs['pin'].getDOMNode().blur();
}
},
forcePassphraseUnlock: function() {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'forcePassphraseUnlock');
},
//-------------------------------------------------------------------------
setInitialFocus: function () {
if (this.props.mode == 'PIN') {
this.refs['pin'].getDOMNode().select();
if (this.mode() == 'PIN') {
this.refs['pin'].getDOMNode().focus();
} else {
this.refs['passphrase'].getDOMNode().select();
this.refs['passphrase'].getDOMNode().focus();
}
},
@ -91,6 +128,35 @@ Clipperz.PM.UI.Components.Pages.UnlockPageClass = React.createClass({
]);
},
pinForm: function () {
return React.DOM.form({
'className':'pinForm pin',
'autoComplete':'off',
}, [
React.DOM.div({'key':'pinFormDiv'},[
React.DOM.label({'htmlFor':'pin'}, "Enter your PIN"),
React.DOM.input({
'type':'tel',
'name':'pin',
'ref':'pin',
'id': 'pinValue',
'className': 'pinValue',
'placeholder':"PIN",
'key':'pin',
'autoCapitalize':'none',
'value': this.state['pin'],
'onChange': this.handlePinChange,
}),
// React.DOM.div({'className': 'pinContainer'}, this.pinFormDigits()),
React.DOM.a({
'className': 'passphraseLogin',
'onClick': this.forcePassphraseUnlock,
}, "Unlock with passphrase")
]),
// React.DOM.button({'key':'submitButton', 'type':'submit', 'disabled':this.props.disabled, 'className':'button'}, "login")
]);
},
shouldEnableUnlockButton: function () {
var result;
@ -103,6 +169,10 @@ Clipperz.PM.UI.Components.Pages.UnlockPageClass = React.createClass({
!this.props['disabled'];
},
// componentDidUpdate: function() {
// this.setInitialFocus();
// },
render: function() {
return React.DOM.div({'key':'unlockForm', 'className':'unlockForm content ' + this.props['style']}, [
Clipperz.PM.UI.Components.AccountStatus(MochiKit.Base.update(this.props['proxyInfo'])),
@ -114,7 +184,7 @@ Clipperz.PM.UI.Components.Pages.UnlockPageClass = React.createClass({
]),
React.DOM.div({'key':'formWrapper', 'className':'form body'}, [
React.DOM.div({'className':'bodyContent'}, [
this.props.mode == 'PIN' ? this.pinForm() : this.loginForm(),
this.mode() == 'PIN' ? this.pinForm() : this.loginForm(),
]),
]),
React.DOM.footer({'key':'footer'}, [

View File

@ -162,20 +162,14 @@ Clipperz.PM.UI.Components.Panels.ExtraFeaturesPanelClass = React.createClass({
// React.DOM.p({}, "Manage your OTPs.")
// ])
]),
/*
React.DOM.li({'key':'account_3', 'onClick':this.toggleExtraFeatureComponent('DevicePIN')}, [
React.DOM.h2({}, "Device PIN"),
React.DOM.div({}, [
React.DOM.p({}, "Configure a PIN that will allow to get access to your cards, but only on this device.")
])
// React.DOM.div({}, [
// React.DOM.p({}, "Configure a PIN that will allow to get access to your cards, but only on this device.")
// ])
]),
React.DOM.li({'key':'account_4'}, [
React.DOM.h2({}, "Preferences"),
React.DOM.div({}, [
React.DOM.p({}, "")
])
]),
*/
React.DOM.li({'key':'account_5', 'onClick':this.toggleExtraFeatureComponent('DeleteAccount'), 'className':(this.state['extraFeatureComponentName'] == 'DeleteAccount') ? 'selected' : ''}, [
React.DOM.h2({}, "Delete account"),
// React.DOM.div({}, [

View File

@ -293,7 +293,7 @@ MochiKit.Base.update(Clipperz.PM.UI.ImportContext.prototype, {
if (isUploadingFile) {
var isExportContent;
isExportContent = new RegExp('.*<textarea>(.*)<\/textarea>.*', 'g');
isExportContent = new RegExp('[\\s\\S]*<textarea>([\\s\\S]*)<\/textarea>[\\s\\S]*', 'g');
if (isExportContent.test(aValue)) {
textarea = MochiKit.DOM.TEXTAREA();
textarea.innerHTML = aValue.replace(isExportContent, '$1');

View File

@ -68,6 +68,7 @@ Clipperz.PM.UI.MainController = function() {
'doLogin', 'registerNewUser', 'showRegistrationForm', 'goBack',
'logout',
'enableLock', 'disableLock', 'unlock',
'updatePIN', 'disablePIN', 'forcePassphraseLogin', 'forcePassphraseUnlock',
'changePassphrase', 'deleteAccount',
/*'updateUserPreferences',*/ 'setPreference',
'updateOTPListAndDetails', 'createNewOTP', 'deleteOTPs', 'changeOTPLabel',
@ -328,6 +329,16 @@ Clipperz.log("THE BROWSER IS OFFLINE");
MochiKit.Async.callLater(0.5, MochiKit.Base.method(registrationPage, 'setInitialFocus'));
},
forcePassphraseLogin_handler: function() {
this.pages()['loginPage'].setProps({'forceCredentials': true});
MochiKit.Async.callLater(0.1, MochiKit.Base.method(this.pages()['loginPage'], 'setInitialFocus'));
},
forcePassphraseUnlock_handler: function() {
this.pages()['unlockPage'].setProps({'forceCredentials': true});
MochiKit.Async.callLater(0.1, MochiKit.Base.method(this.pages()['unlockPage'], 'setInitialFocus'));
},
//=========================================================================
doLogin_handler: function (event) {
@ -411,35 +422,57 @@ Clipperz.log("THE BROWSER IS OFFLINE");
return deferredResult;
},
unlock_handler: function(aPassphrase) {
unlock_handler: function(aCredential, aCredentialType) {
var deferredResult;
var passphrase;
var user = this.user();
var unlockPage = this.pages()['unlockPage'];
var overlay = this.overlay();
passphrase = (aCredentialType=='PIN') ? Clipperz.PM.PIN.credentialsWithPIN(aCredential)['passphrase'] : aCredential;
overlay.show("validating…");
deferredResult = new Clipperz.Async.Deferred('MainController.unlock_handler', {trace:false});
deferredResult.addMethod(unlockPage, 'setProps', {'disabled': true});
deferredResult.addMethod(user, 'unlock', function() { return MochiKit.Async.succeed(aPassphrase); });
deferredResult.addErrback(function (aValue) {
deferredResult.addMethod(user, 'unlock', function() { return MochiKit.Async.succeed(passphrase); });
deferredResult.addErrback(MochiKit.Base.bind(function (aValue) {
var innerDeferredResult;
var errorMessage;
errorMessage = 'failed';
if (aCredentialType=='PIN') {
var attemptsLeft = Clipperz.PM.PIN.recordFailedAttempt();
if (attemptsLeft == -1) {
errorMessage = 'PIN resetted';
}
}
innerDeferredResult = new Clipperz.Async.Deferred('MainController.unlock_handler <incorrect passphrase>', {trace:false});
innerDeferredResult.addMethod(unlockPage, 'setProps', {'disabled': false});
innerDeferredResult.addMethod(unlockPage, 'setProps', {
'disabled': false,
'mode': this.loginMode(),
});
innerDeferredResult.addMethod(unlockPage, 'setInitialFocus');
innerDeferredResult.addMethod(overlay, 'failed', "", 1);
innerDeferredResult.addMethod(overlay, 'failed', errorMessage, 1);
innerDeferredResult.addCallback(MochiKit.Async.fail, aValue);
innerDeferredResult.callback();
return aValue;
});
}, this));
if (aCredentialType=='PIN') {
deferredResult.addMethod(Clipperz.PM.PIN, 'resetFailedAttemptCount');
}
deferredResult.addMethod(this, 'updateUserPreferences');
deferredResult.addMethod(this, 'moveInPage', this.currentPage(), 'mainPage');
deferredResult.addMethod(this, 'refreshUI');
deferredResult.addMethod(unlockPage, 'setProps', {'disabled': false});
deferredResult.addMethod(unlockPage, 'setProps', {
'disabled': false,
'forceCredentials': false,
});
deferredResult.addMethod(unlockPage, 'resetUnlockForm');
deferredResult.addCallback(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'enableLock');
deferredResult.addMethod(overlay, 'done', "", 0.5);
@ -448,7 +481,7 @@ Clipperz.log("THE BROWSER IS OFFLINE");
return deferredResult;
// this.user().setPassphraseFunction(function(){return aPassphrase;});
// this.user().setPassphraseFunction(function(){return passphrase;});
// TODO: check if passphrase is correct by try/catch on decrypting something
// this.moveOutPage(this.currentPage(), 'mainPage');
// TODO: check why the unlock form keeps the value stored (doesn't happen with the login form...)
@ -816,12 +849,29 @@ Clipperz.log("THE BROWSER IS OFFLINE");
MochiKit.Signal.disconnectAll(Clipperz.Signal.NotificationCenter, 'lock');
},
updatePIN_handler: function(aPIN) {
return Clipperz.Async.callbacks("MainController.updatePIN_handler", [
MochiKit.Base.method(this.overlay(), 'show', "updating …", true),
MochiKit.Base.method(Clipperz.PM.PIN, 'updatePin', this.user(), aPIN),
MochiKit.Base.method(this.overlay(), 'done', "saved", 1)
], {trace:false});
},
disablePIN_handler: function() {
return Clipperz.Async.callbacks("MainController.disablePIN_handler", [
MochiKit.Base.method(this.overlay(), 'show', "disabling …", true),
MochiKit.Base.method(Clipperz.PM.PIN, 'disablePin'),
MochiKit.Base.method(this.overlay(), 'done', "saved", 1)
], {trace:false});
},
resetLockTimeout: function () {
if (this.user()) {
return Clipperz.Async.callbacks("MainController.resetLockTimeout", [
MochiKit.Base.method(this.user(), 'getPreference', 'lock'),
MochiKit.Base.bind(function (someLockInfo) {
if (this._lockTimeout) {
// console.log("clearing previous lock timer");
clearTimeout(this._lockTimeout);
}
@ -990,7 +1040,7 @@ Clipperz.log("THE BROWSER IS OFFLINE");
errorMessage = "failure";
} else {
if ('pin' in anEvent) {
errorCount = Clipperz.PM.PIN.recordFailedAttempt();
var errorCount = Clipperz.PM.PIN.recordFailedAttempt();
if (errorCount == -1) {
errorMessage = "PIN resetted";
}
@ -1170,7 +1220,8 @@ Clipperz.log("THE BROWSER IS OFFLINE");
'style': this.mediaQueryStyle(),
'isTouchDevice': this.isTouchDevice(),
'isDesktop': this.isDesktop(),
'hasKeyboard': this.hasKeyboard()
'hasKeyboard': this.hasKeyboard(),
'PIN': Clipperz.PM.PIN
};
},
@ -1186,11 +1237,15 @@ Clipperz.log("THE BROWSER IS OFFLINE");
if (aPageName == 'loginPage') {
extraProperties = {
'mode': 'CREDENTIALS',
'mode': this.loginMode(),
'isNewUserRegistrationAvailable': Clipperz.PM.Proxy.defaultProxy.canRegisterNewUsers(),
'disabled': false,
'proxyInfo': this.proxyInfo(),
};
} else if (aPageName == 'unlockPage') {
extraProperties = {
'mode': this.loginMode(),
}
} else if (aPageName == 'registrationPage') {
} else if (aPageName == 'mainPage') {
extraProperties = {

View File

@ -51,8 +51,8 @@
"MochiKit/Selector.js",
"-- MochiKit/Visual.js",
"-- React/react-0.13.3.js",
"React/react.min-0.13.3.js",
"React/react-0.13.3.js",
"-- React/react.min-0.13.3.js",
"-- #React/react-with-addons-0.13.1.js",
"-- #React/react-with-addons.min-0.13.1.js",

View File

@ -305,6 +305,43 @@ refer to http://www.clipperz.com.
}
}
}
.pinForm {
/*
.pinContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
.pinDigit {
width: 15%;
}
}
*/
label {
display: inherit;
text-align: left;
}
.pinValue {
font-family: clipperz-password;
@include placeholder {
font-family: clipperz-font;
}
}
.passphraseLogin {
font-size: .9em;
color: $clipperz-orange;
text-decoration: underline;
cursor:pointer;
}
}
}
.other {

View File

@ -251,6 +251,10 @@ refer to http://www.clipperz.com.
p {
padding: 10px 0px;
}
strong {
font-weight: bold;
}
}
form {
@ -706,6 +710,25 @@ refer to http://www.clipperz.com.
}
}
.devicePIN {
.pinDigit {
display: inline-block;
width: 2em;
margin-right: 0.5em;
text-align: center;
}
.pinValue {
display: inline-block;
width: 4em;
margin-right: 1em;
}
:enabled {
border:2px solid $clipperz-orange;
}
}
.dataImport {
.content {