Merge commit '73e974ce936d1261f7310250388adf115380fc07'

* commit '73e974ce936d1261f7310250388adf115380fc07':
  Add custom url scheme 'padlock://'
  Bump version number to 1.1.0
  Validate email before sending connection request
  Remove 'faint' color style from connection id in cloud view for better readability
  Disable inserting/removing billing key from cordova hooks since we're no longer using purchase plugin
  Remove option to request data reset; replace with link to dashboard instead
  Increase interval for polling auth token activation from 1 to 3 seconds
  Only show subscription-relevant controls if subscription status is set; test padlock cloud credentials every time when opening cloud view
  Handle expired auth token same as invalid auth token; fix typo
  send client version via X-Client-Version header with each request
  Switch from using http status codes for identifying errors to json-encoded error codes; Implement new subscription flow (no more in-app purchases)
This commit is contained in:
Martin Kleinschrodt 2016-11-02 19:39:23 +01:00
commit 3ab80a2551
10 changed files with 147 additions and 176 deletions

View File

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="com.maklesoft.padlock" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<widget id="com.maklesoft.padlock" version="1.1.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>Padlock</name>
<description>
</description>
@ -19,8 +19,6 @@
<preference name="StatusBarOverlaysWebView" value="true" />
<preference name="StatusBarStyle" value="lightcontent" />
<hook src="scripts/build-www.js" type="before_prepare" />
<hook src="scripts/insert-billing-key.js" type="before_prepare" />
<hook src="scripts/remove-billing-key.js" type="after_prepare" />
<platform name="android">
<icon density="ldpi" src="res/android/icon/app-icon-ldpi.png" />
<icon density="mdpi" src="res/android/icon/app-icon-mdpi.png" />
@ -70,9 +68,6 @@
<splash height="1242" src="res/ios/splash/splash-2208x1242.png" width="2208" />
</platform>
<plugin name="com.verso.cordova.clipboard" spec="https://github.com/VersoSolutions/CordovaClipboard.git" />
<plugin name="cc.fovea.cordova.purchase" spec="https://github.com/j3k0/cordova-plugin-purchase.git">
<variable name="BILLING_KEY" value="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlX8cOgzEEOPmPOwneWjNutvBCYCjDsAlX0Q8Ulf3biZKb2V1BIg72sx" />
</plugin>
<plugin name="com.wongatech.cordova-disable-nsurl-cache" spec="https://github.com/MaKleSoft/cordova-disable-nsurl-cache.git" />
<preference name="xwalkVersion" value="19+" />
<preference name="xwalkCommandLine" value="--disable-pull-to-refresh-effect" />
@ -88,4 +83,7 @@
<engine name="ios" spec="~4.2.0" />
<plugin name="cordova-plugin-backbutton" spec="~0.3.0" />
<engine name="android" spec="~5.2.2" />
<plugin name="cordova-plugin-customurlscheme" spec="~4.2.0">
<variable name="URL_SCHEME" value="padlock" />
</plugin>
</widget>

View File

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Padlock",
"version": "1.0.0",
"version": "1.1.0",
"description": "A minimalist open source password manager.",
"author": "Martin Kleinschrodt",
"app": {

View File

@ -4,30 +4,28 @@
padlock.CloudSource = (function(Source) {
"use strict";
padlock.ERR_CLOUD_UNAUTHORIZED = "Not authorized to request from source";
padlock.ERR_CLOUD_SERVER_ERROR = "Internal server error";
padlock.ERR_CLOUD_FAILED_CONNECTION = "Failed connection";
padlock.ERR_CLOUD_VERSION_DEPRECATED = "Api version deprecated";
padlock.ERR_CLOUD_SUBSCRIPTION_REQUIRED = "Padlock Cloud subscription required";
padlock.ERR_CLOUD_NOT_FOUND = "Account not found";
padlock.ERR_CLOUD_LIMIT_EXCEEDED = "Rate limit exceeded";
padlock.ERR_CLOUD_INVALID_AUTH_TOKEN = "invalid_auth_token";
padlock.ERR_CLOUD_EXPIRED_AUTH_TOKEN = "expired_auth_token";
padlock.ERR_CLOUD_SERVER_ERROR = "internal_server_error";
padlock.ERR_CLOUD_VERSION_DEPRECATED = "deprecated_api_version";
padlock.ERR_CLOUD_SUBSCRIPTION_REQUIRED = "subscription_required";
padlock.ERR_CLOUD_NOT_FOUND = "account_not_found";
padlock.ERR_CLOUD_LIMIT_EXCEEDED = "rate_limit_exceeded";
padlock.ERR_CLOUD_FAILED_CONNECTION = "failed_connection";
padlock.ERR_CLOUD_UNKNOWN = "unknown_error";
function errFromStatus(s) {
switch(s) {
case 401:
return padlock.ERR_CLOUD_UNAUTHORIZED;
case 402:
return padlock.ERR_CLOUD_SUBSCRIPTION_REQUIRED;
case 404:
return padlock.ERR_CLOUD_NOT_FOUND;
case 406:
return padlock.ERR_CLOUD_VERSION_DEPRECATED;
case 429:
return padlock.ERR_CLOUD_LIMIT_EXCEEDED;
case 0:
return padlock.ERR_CLOUD_FAILED_CONNECTION;
default:
return padlock.ERR_CLOUD_SERVER_ERROR;
function errFromReq(req) {
if (req.status == 0) {
return { error: padlock.ERR_CLOUD_FAILED_CONNECTION };
} else {
try {
return JSON.parse(req.responseText);
} catch (e) {
return {
error: "unknown_error",
message: req.responseText
};
}
}
}
@ -54,9 +52,15 @@ padlock.CloudSource = (function(Source) {
req.onreadystatechange = function() {
if (req.readyState === 4) {
this.settings["sync_sub_status"] = req.getResponseHeader("X-Sub-Status");
try {
this.settings["sync_trial_end"] = parseInt(req.getResponseHeader("X-Sub-Trial-End"), 10);
} catch (e) {
//
}
cb(req);
}
};
}.bind(this);
try {
req.open(method, url, true);
@ -67,9 +71,7 @@ padlock.CloudSource = (function(Source) {
"AuthToken " + this.settings.sync_email + ":" + this.settings.sync_key);
}
if (this.settings.sync_require_subscription === false) {
req.setRequestHeader("Require-Subscription", "NO");
}
req.setRequestHeader("X-Client-Version", padlock.version);
return req;
} catch(e) {
@ -82,7 +84,7 @@ padlock.CloudSource = (function(Source) {
if (isSuccess(req.status)) {
this.didFetch(req.responseText, opts);
} else if (opts && opts.fail) {
opts.fail(errFromStatus(req.status));
opts.fail(errFromReq(req));
}
}.bind(this));
@ -96,15 +98,17 @@ padlock.CloudSource = (function(Source) {
CloudSource.prototype.save = function(opts) {
var req = this.prepareRequest("PUT", "/store/", function(req) {
if (isSuccess(req.status)) {
this.didFetch(req.responseText, opts);
if (isSuccess(req.status) && opts && opts.success) {
opts.success();
} else if (opts && opts.fail) {
opts.fail(errFromStatus(req.status));
opts.fail(errFromReq(req));
}
}.bind(this));
if (!req) {
opts && opts.fail(padlock.ERR_CLOUD_FAILED_CONNECTION);
opts && opts.fail({
error: padlock.ERR_CLOUD_FAILED_CONNECTION
});
return;
}
@ -127,7 +131,7 @@ padlock.CloudSource = (function(Source) {
fail(padlock.ERR_CLOUD_SERVER_ERROR);
}
} else {
fail && fail(errFromStatus(req.status));
fail && fail(errFromReq(req));
}
});
@ -140,23 +144,6 @@ padlock.CloudSource = (function(Source) {
req.send("email=" + encodeURIComponent(email));
};
CloudSource.prototype.requestDataReset = function(success, fail) {
var req = this.prepareRequest("DELETE", "/store/", function(req) {
if (isSuccess(req.status)) {
success && success();
} else {
fail && fail(errFromStatus(req.status));
}
});
if (!req) {
fail && fail(padlock.ERR_CLOUD_FAILED_CONNECTION);
return;
}
req.send();
};
CloudSource.prototype.testCredentials = function(success, fail) {
var req = this.prepareRequest("HEAD", "/store/", function() {
if (isSuccess(req.status)) {
@ -164,7 +151,7 @@ padlock.CloudSource = (function(Source) {
} else if (req.status == 401) {
success && success(false);
} else {
fail && fail(errFromStatus(req.status));
fail && fail(errFromReq(req));
}
});
@ -176,7 +163,5 @@ padlock.CloudSource = (function(Source) {
req.send();
};
CloudSource.errFromStatus = errFromStatus;
return CloudSource;
})(padlock.Source);

View File

@ -35,7 +35,8 @@ padlock.Settings = (function(util, LocalSource) {
"sync_device": "",
"sync_connected": false,
"sync_auto": true,
"sync_readonly": false,
"sync_sub_status": "",
"sync_trial_end": 0,
"default_fields": ["username", "password"],
"obfuscate_fields": false,
"showed_backup_reminder": 0,

View File

@ -5,7 +5,7 @@
* Top-level component for rendering application interface. Requires a `padlock.Collection` and
* `padlock.Settings` object to be passed into the constructor as dependencies.
*/
padlock.App = (function(Polymer, platform, pay) {
padlock.App = (function(Polymer, platform) {
"use strict";
return Polymer({
@ -506,9 +506,12 @@ padlock.App = (function(Polymer, platform, pay) {
} else {
this.$.notification.show("Synchronization successful!", "success", 2000);
}
// Some settings might have been updated during the request
this._notifySettings();
}.bind(this),
fail: function(e) {
switch (e) {
var err = typeof e === "string" ? e : e.error;
switch (err) {
case padlock.ERR_SOURCE_INVALID_JSON:
case padlock.ERR_CRYPTO_INVALID_KEY_PARAMS:
case padlock.ERR_CRYPTO_INVALID_CONTAINER:
@ -525,8 +528,7 @@ padlock.App = (function(Polymer, platform, pay) {
this._requireRemotePassword();
break;
case padlock.ERR_CLOUD_SUBSCRIPTION_REQUIRED:
this.set("settings.sync_readonly", true);
this._alertReadonly();
this._alertSubscriptionRequired();
break;
default:
this._handleError(e);
@ -534,6 +536,8 @@ padlock.App = (function(Polymer, platform, pay) {
// Hide progress indicator
this.$.synchronizing.hide();
// Some settings might have been updated during the request
this._notifySettings();
}.bind(this)
});
} else {
@ -831,8 +835,9 @@ padlock.App = (function(Polymer, platform, pay) {
this._handleError(e.detail);
},
_handleError: function(e) {
switch (e) {
case padlock.ERR_CLOUD_UNAUTHORIZED:
switch (typeof e === "string" ? e : e.error) {
case padlock.ERR_CLOUD_INVALID_AUTH_TOKEN:
case padlock.ERR_CLOUD_EXPIRED_AUTH_TOKEN:
this.set("settings.sync_connected", false);
this.set("settings.sync_key", "");
this.set("settings.sync_email", "");
@ -857,13 +862,6 @@ padlock.App = (function(Polymer, platform, pay) {
"Please note that you won't be able to use Padlock Cloud until you install the latest version!"
);
break;
case padlock.ERR_PAY_INVALID_RECEIPT:
this._openForm([
{element: "button", label: "Try Again",
tap: this._buySubscription.bind(this), submit: true},
{element: "button", label: "Dismiss", cancel: true}
], "We were unable to verify your purchase. Please try again!");
break;
case padlock.ERR_CLOUD_LIMIT_EXCEEDED:
this._alert("Padlock Cloud is over capacity right now. Please try again in a few minutes!");
break;
@ -875,37 +873,18 @@ padlock.App = (function(Polymer, platform, pay) {
_openAppStore: function() {
window.open(platform.getAppStoreLink(), "_system");
},
_alertReadonly: function() {
_alertSubscriptionRequired: function() {
this._openForm(
[
{element: "button", label: "Renew Subscription", submit: true,
tap: this._buySubscription.bind(this)},
{element: "button", label: "Go To Padlock Cloud Settings", submit: true,
tap: this._openCloudView.bind(this)},
{element: "button", label: "Dismiss", cancel: true}
],
"It seems your Padlock Cloud subscription has expired which means that you can " +
"download your data from the cloud but you won't be able to update it or synchronize with " +
"any other devices. Renew your subscription now to unlock the full potential of Padlock Cloud!"
"You currently don't have an active subscription which means you can access " +
"your existing data on Padlock Cloud but you won't be able to upload any new data " +
"or synchronize changes between devices. Get a subscription now to regain full access " +
"to Padlock Cloud!"
);
},
_buySubscription: function() {
this.$.connecting.show();
pay.getProductInfo(function(info) {
this.$.connecting.hide();
this._openForm(
[
{element: "button", label: "Buy Subscription (" + info.price + " / month)", submit: true},
{element: "button", label: "No Thanks", cancel: true}
],
info.description,
function() {
pay.orderSubscription(this.settings.sync_host_url, this.settings.sync_email,
this._subscriptionVerified.bind(this), this._handleError.bind(this));
}.bind(this)
);
}.bind(this));
},
_selectMarkedRecord: function() {
this._currentView.selectMarked && this._currentView.selectMarked();
},
@ -948,4 +927,4 @@ padlock.App = (function(Polymer, platform, pay) {
}
});
})(Polymer, padlock.platform, padlock.pay);
})(Polymer, padlock.platform);

View File

@ -37,17 +37,27 @@
</section>
<section hidden$="{{ !settings.sync_connected }}">
<div class="note" hidden$="{{ settings.sync_readonly }}">
<div class="note">
<strong>Connected</strong> - This device is connected to the Padlock Cloud account <strong>{{ settings.sync_email }}</strong>.
Connect all your devices with the same account to easily synchronize your data between them!
<div class="conn-id">Connection ID: {{ settings.sync_id }}</div>
</div>
<div class="note" hidden$="{{ !settings.sync_readonly }}">
<strong>Subscription expired</strong> - It seems your Padlock Cloud subscription for the account <strong>{{ settings.sync_email }}</strong> has expired which means that you can download your data from the cloud but you won't be able to update it or synchronize with any other devices. Renew your subscription now to unlock the full potential of Padlock Cloud!
<div class="note" hidden$="{{ !_isTrialing(settings.sync_sub_status) }}">
<strong>Your trial period ends in {{ _remainingTrialDays(settings.sync_trial_end) }} days.</strong>
After this period, your access will be read-only, which means you will be able to access
your existing data on Padlock Cloud but you won't be able to upload any new data or synchronize
changes between devices. Get a subscription now to get unlimited access to Padlock Cloud!
</div>
<button hidden$="{{ !settings.sync_readonly }}" on-tap="_buySubscription">Renew Subscription</button>
<div class="note" hidden$="{{ !_isInactive(settings.sync_sub_status) }}">
<strong>Read-Only</strong> -
You currently don't have an active subscription! This means you can access
your existing data on Padlock Cloud but you won't be able to upload any new data
or synchronize changes between devices. Get a subscription now to regain full access
to Padlock Cloud!
</div>
<button on-tap="_openDashboard" hidden$="{{ !_isInactive(settings.sync_sub_status) }}">Manage Subscriptions</button>
<button on-tap="_synchronize">Synchronize</button>
<button on-tap="_resetRemoteData">Reset Data</button>
<button on-tap="_openDashboard">Manage Account</button>
<button on-tap="_disconnect">Disconnect</button>
<padlock-toggle-button value="{{ settings.sync_auto }}" label="Auto Sync"></padlock-toggle-button>
</section>
@ -108,7 +118,7 @@
{element: "button", label: "Cancel", cancel: true}
],
title: "Connect to Padlock Cloud",
submit: this.requestAuthToken.bind(this)
submit: this._connectEnter.bind(this)
});
},
_disconnect: function() {
@ -126,20 +136,24 @@
}.bind(this)
});
},
//* Requests an api key from the cloud api with the entered email and device name
requestAuthToken: function(data) {
_connectEnter: function(data, inputs) {
var email = data.email;
var create = data.create;
if (!email) {
var message = "Please enter an email address!";
this.fire("notify", {
message: message,
duration: 2000
if (!email || !inputs.email.checkValidity()) {
this.fire("open-form", {
components: [
{element: "button", label: "Try Again", submit: true},
],
title: email ? inputs.email.validationMessage : "Please enter an email address!",
submit: this.connect.bind(this)
});
return;
}
this._requestAuthToken(email);
},
//* Requests an api key from the cloud api with the entered email and device name
_requestAuthToken: function(email, create) {
this.set("settings.sync_email", email);
this.set("settings.sync_key", "");
@ -158,62 +172,32 @@
this._alert("Almost done! An email was sent to " + email + " with further instructions.");
}.bind(this), function(e) {
this.$$("padlock-progress").hide();
switch(e) {
switch (typeof e === "string" ? e : e.error) {
case padlock.ERR_CLOUD_NOT_FOUND:
data.create = true;
this.requestAuthToken(data);
break;
case padlock.ERR_CLOUD_SUBSCRIPTION_REQUIRED:
this.fire("open-form", {
title: "No existing Padlock Cloud account was found. In order to create a new one, " +
"you need a Padlock Cloud subscription.",
components: [
{element: "button", label: "Continue", submit: true},
{element: "button", label: "Cancel", cancel: true}
],
submit: this._buySubscription.bind(this)
});
this._requestAuthToken(email, true);
break;
case padlock.ERR_CLOUD_LIMIT_EXCEEDED:
this._alert("For security reasons only one connection request is allowed per " +
"minute. Please wait a little before trying again!");
this._alert("For security reasons only a limited amount of connection request " +
"are allowed at a time. Please wait a little before trying again!");
break;
default:
this.fire("error", e);
}
this.notifyPath("settings.sync_sub_status");
this.notifyPath("settings.sync_trial_end");
}.bind(this));
},
_isActivationPending: function() {
return this.settings.sync_key && !this.settings.sync_connected;
},
_resetRemoteData: function() {
this.fire("open-form", {
components: [
{element: "button", label: "Reset", submit: true},
{element: "button", label: "Cancel", cancel: true}
],
title: "Are you sure you want to reset all your data on Padlock Cloud?",
submit: this._requestResetRemoteData.bind(this)
});
_isTrialing: function(s) {
return s == "trialing";
},
_requestResetRemoteData: function() {
var email = this.settings.sync_email;
var cloudSource = new CloudSource(this.settings);
this.$$("padlock-progress").show();
cloudSource.requestDataReset(function() {
this.$$("padlock-progress").hide();
this._alert("Almost done! An email was sent to " + email + " with further instructions.");
}.bind(this), function(e) {
if (e === padlock.ERR_CLOUD_LIMIT_EXCEEDED) {
this._alert("For security reasons only one deletion request is allowed per " +
"minute. Please wait a little before trying again!");
} else {
this.$$("padlock-progress").hide();
this.fire("error", e);
}
}.bind(this));
_isActive: function(s) {
return s == "active";
},
_isInactive: function(s) {
return s && s != "active" && s != "trialing";
},
//* Shows an alert dialog with a given _message_
_alert: function(message) {
@ -226,12 +210,16 @@
this.set("settings.sync_connected", connected);
if (connected) {
this._connectionSuccess();
this.notifyPath("settings.sync_sub_status", this.settings.sync_sub_status);
this.notifyPath("settings.sync_trial_end", this.settings.sync_trial_end);
if (poll) {
this._connectionSuccess();
}
}
if (!connected && poll && this.settings.sync_key) {
clearTimeout(this._testCredsTimeout);
this._testCredsTimeout = setTimeout(this._testCredentials.bind(this, true), 1000);
this._testCredsTimeout = setTimeout(this._testCredentials.bind(this, true), 3000);
}
}.bind(this), this.fire.bind(this, "error"));
},
@ -239,8 +227,8 @@
clearTimeout(this._testCredsTimeout);
},
_showingChanged: function() {
if (this.showing && this.settings.sync_key && !this.settings.sync_connected) {
this._testCredentials(true);
if (this.showing && this.settings.sync_key) {
this._testCredentials(!this.settings.sync_connected);
} else if (!this.showing) {
this._stopTestCredentials();
}
@ -280,9 +268,6 @@
_customUrlChanged: function() {
this.$.customUrlInput.checkValidity();
},
_buySubscription: function() {
this.fire("buy-subscription");
},
_cancelConnect: function() {
this.fire("open-form", {
title: "Are you sure you want to cancel the connection process?",
@ -295,11 +280,18 @@
this._stopTestCredentials();
}.bind(this)
});
},
_openDashboard: function() {
window.open(this.settings["sync_host_url"] + "/dashboard/", "_system");
},
_remainingTrialDays: function(trialEnd) {
var now = new Date().getTime() / 1000;
trialEnd = trialEnd ? parseInt(trialEnd, 10) : now;
return Math.ceil((trialEnd - now) / 60 / 60 / 24);
}
});
})(Polymer, padlock.ViewBehavior, padlock.CloudSource, padlock.pay);
})(Polymer, padlock.ViewBehavior, padlock.CloudSource);
</script>
</dom-module>

View File

@ -40,7 +40,6 @@ button, padlock-toggle-button {
.conn-id {
margin-top: 10px;
font-weight: bold;
color: $text-color-faint;
}
#customUrlInput {

View File

@ -97,12 +97,22 @@
return vals;
}, {});
},
_inputs: function() {
return Polymer.dom(this.root).querySelectorAll("input").reduce(function(els, el) {
var name = el.getAttribute("name");
if (name) {
els[name] = el;
}
return els;
}, {});
},
_submit: function(e) {
this.blurInputElements();
var values = this._values();
var inputs = this._inputs();
if (typeof this.submitCallback == "function") {
this.submitCallback(values);
this.submitCallback(values, inputs);
}
this.fire("form-submit", values);

View File

@ -103,14 +103,18 @@
this.mode = this.mode == "restore-cloud" ? "get-started" : "restore-cloud";
},
_cloudEnter: function() {
var cloudSource = new CloudSource(this.settings);
var input = this.$.emailInput;
var email = input.value;
var email = this.$.emailInput.value;
if (!email) {
this.fire("notify", {message: "Please enter an email address!", type: "error", duration: 2000});
if (!email || !input.checkValidity()) {
this.fire("alert", {
message: email ? input.validationMessage : "Please enter an email address!"
});
return;
}
var cloudSource = new CloudSource(this.settings);
this.$$("padlock-progress").show();
this.$.cloudEnterButton.disabled = true;
cloudSource.requestAuthToken(email, false, function(authToken) {
@ -124,7 +128,7 @@
}.bind(this), function(e) {
this.$.cloudEnterButton.disabled = false;
this.$$("padlock-progress").hide();
switch(e) {
switch (typeof e === "string" ? e : e.error) {
case padlock.ERR_CLOUD_NOT_FOUND:
case padlock.ERR_CLOUD_SUBSCRIPTION_REQUIRED:
this.fire("open-form", {
@ -163,8 +167,8 @@
this.set("settings.sync_id", "");
return;
}
if (e == padlock.ERR_CLOUD_UNAUTHORIZED) {
this._attemptRestoreTimeout = setTimeout(this._attemptRestore.bind(this), 1000);
if (e.error == padlock.ERR_CLOUD_INVALID_AUTH_TOKEN) {
this._attemptRestoreTimeout = setTimeout(this._attemptRestore.bind(this), 3000);
} else {
this.fire("error", e);
}
@ -184,6 +188,8 @@
return;
}
this.set("settings.sync_connected", true);
this.notifyPath("settings.sync_sub_status", this.settings.sync_sub_status);
this.notifyPath("settings.sync_trial_end", this.settings.sync_trial_end);
this.collection.save({password: this.$.cloudPwdInput.value, rememberPassword: true});
this.fire("open-form", {

View File

@ -1,6 +1,8 @@
<script>
/* global window, padlock */
window.padlock = {};
window.padlock = {
version: "1.1.0"
};
</script>
<script src="../lib/sjcl.js"></script>
<script src="../bower_components/zxcvbn/lib/zxcvbn.js"></script>
@ -9,7 +11,6 @@
<script src="rand.js"></script>
<script src="crypto.js"></script>
<script src="platform.js"></script>
<script src="pay.js"></script>
<script src="Source.js"></script>
<script src="LocalStorageSource.js"></script>
<script src="ChromeStorageSource.js"></script>