Compare commits

...

9 Commits

Author SHA1 Message Date
Bruno Bernardino 9b66955962
Merge branch 'main' into feature/email-notifications-failed-login 2022-08-02 10:20:45 +01:00
Bruno Bernardino 4b46a5f766
Tweak icons for email notifications 2022-08-02 10:20:05 +01:00
Bruno Bernardino ed550d71a5
Remove SRP when failing login 5 times, restart on 6th attempt. 2022-08-02 10:11:40 +01:00
Bruno Bernardino e0933dbfc1
Add changelog, remove SRP session after 5 failed logins, tweak email text. 2022-08-02 09:16:08 +01:00
Bruno Bernardino df18e71e6a
Apply requested changes, tweaking text, adding alert, adding and removing awaits. 2022-08-02 07:49:15 +01:00
Bruno Bernardino 8d0a255ce7
Remove failed unlock notification, add new login notification
Also address the other PR requests and tweak the legacy UI tests
2022-08-01 12:14:51 +01:00
Bruno Bernardino 052477b6c9
Merge branch 'main' into feature/email-notifications-failed-login 2022-08-01 08:47:52 +01:00
Bruno Bernardino d351f80ac6
Add location and device information to email, remove trusted device after 5 failed attempts 2022-07-22 12:13:21 +01:00
Bruno Bernardino d1f3295879
Email Notifications: Failed Login Attempt
This is a new feature, related to #346 (half of it).

I'm unsure if we should implement a new server method to be called when the client is already authorized but fails to unlock (a trusted device has been compromised, for example), because that could be helpful but also easy to prevent from being called, so not entirely reliable.
2022-07-21 12:36:31 +01:00
16 changed files with 824 additions and 74 deletions

View File

@ -6,40 +6,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to
[Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## 3.1.2
## 4.1.0
- Fixes bug that caused values from previously created vault item to be
pre-filled when creating the next item.
- Fixes attachment previews on Android
- (feature): Account activity email notifications!
## 3.1.1
You will now receive email notifications when there's been 5 failed login
attempts, and another when there's been a successful login from a new or
untrusted device. You can disable these from your security settings.
- Fixes various bugs related to shared vault syncing and billing.
## 4.0.0
## 3.1.0
### New Stuff & Improvements
- Improved flow for creating a vault item
- If a vault filter is active, preselect that vault during vault item creation
- Prefill field names with sensible default when adding new field
- Automated account migration if legacy account is detected during
login/signup
- "Login" vault item template is now called "Website / App"
- Added new vault item template "Computer"
- [DESKTOP] Ctrl/cmd + Shift + F to search all items (resetting any active
filters)
- [ANDROID] Allow reordering fields via drag and drop on Android
- [SERVER] Option to enable secure connection when sending emails, enabled via
`PL_EMAIL_SECURE` environment variable
### Bug Fixes
- Sometimes the app would show a blank screen directly after unlocking.
- Changes made to a vault item directly after creating it would sometimes be
discarded.
## 3.0.0
Initial release of Padloc 3 (changes before 3.0.0 are not included in this
change log).
Initial release of Padloc 4 (changes before 4.0.0 are not included in this
change log), though
[some can be seen in this commit](https://github.com/padloc/padloc/blob/12b027b37ccf123b15a066e4715354f4cf080384/CHANGELOG.md).

View File

@ -0,0 +1,300 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{ title }}</title>
<style>
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class="body"] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class="body"] p,
table[class="body"] ul,
table[class="body"] ol,
table[class="body"] td,
table[class="body"] span,
table[class="body"] a {
font-size: 16px !important;
}
table[class="body"] .wrapper,
table[class="body"] .article {
padding: 10px !important;
}
table[class="body"] .content {
padding: 0 !important;
}
table[class="body"] .container {
padding: 0 !important;
width: 100% !important;
}
table[class="body"] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class="body"] .btn table {
width: 100% !important;
}
table[class="body"] .btn a {
width: 100% !important;
}
table[class="body"] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #3498db !important;
}
.btn-primary a:hover {
background-color: #3498db !important;
border-color: #3498db !important;
}
}
</style>
</head>
<body
class=""
style="
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
class="body"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
background-color: #f6f6f6;
"
>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top">&nbsp;</td>
<td
class="container"
style="
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
display: block;
margin: 0 auto;
max-width: 580px;
padding: 10px;
width: 580px;
"
>
<div
class="content"
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px"
>
<!-- START CENTERED WHITE CONTAINER -->
<span
class="preheader"
style="
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
></span>
<table
class="main"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
background: #ffffff;
border-radius: 5px;
"
>
<!-- START MAIN CONTENT AREA -->
<tr>
<td
class="wrapper"
style="
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
box-sizing: border-box;
padding: 20px;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top">
<p
style="
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
"
>
Hi there!
</p>
<p
style="
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
"
>
This is just an email to warn you that there was a
<strong>failed</strong> attempt to login to your account in Padloc,
from {{ location }}.
</p>
<p
style="
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
"
>
If this was you, there's no action necessary, otherwise you might
want to make sure your trusted devices haven't been compromised, or
to remove potentially compromised devices from your trusted list
inside the app.
</p>
<p
style="
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
"
>
Note that if this happened on a trusted device, it was already
removed from the trusted devices list, automatically.
</p>
<p
style="
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
"
>
Have a great day!
</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%">
<table
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
>
<tr>
<td
class="content-block"
style="
font-family: sans-serif;
vertical-align: top;
padding-bottom: 10px;
padding-top: 10px;
font-size: 12px;
color: #999999;
text-align: center;
"
>
<span
class="apple-link"
style="color: #999999; font-size: 12px; text-align: center"
>This email was sent to you by Padloc (https://padloc.app). If you have any
questions, please don't hesitate to contact us at support@padloc.app!</span
>
<!--<br> Don't like these emails? <a href="" style="text-decoration: underline; color: #999999; font-size: 12px; text-align: center;">Unsubscribe</a>.-->
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,11 @@
Hi there!
This is just an email to warn you that there was a failed attempt to login to your account in Padloc, from {{ location }}.
If this was you, there's no action necessary, otherwise you might want to make sure your trusted devices haven't been compromised, or to remove potentially compromised devices from your trusted list inside the app.
Note that if this happened on a trusted device, it was already removed from the trusted devices list, automatically.
Have a great day!
This email was sent to you by Padloc (https://padloc.app). If you have any questions, please don't hesitate to contact us at support@padloc.app!

287
assets/email/new-login.html Normal file
View File

@ -0,0 +1,287 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{ title }}</title>
<style>
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class="body"] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class="body"] p,
table[class="body"] ul,
table[class="body"] ol,
table[class="body"] td,
table[class="body"] span,
table[class="body"] a {
font-size: 16px !important;
}
table[class="body"] .wrapper,
table[class="body"] .article {
padding: 10px !important;
}
table[class="body"] .content {
padding: 0 !important;
}
table[class="body"] .container {
padding: 0 !important;
width: 100% !important;
}
table[class="body"] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class="body"] .btn table {
width: 100% !important;
}
table[class="body"] .btn a {
width: 100% !important;
}
table[class="body"] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #3498db !important;
}
.btn-primary a:hover {
background-color: #3498db !important;
border-color: #3498db !important;
}
}
</style>
</head>
<body
class=""
style="
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
class="body"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
background-color: #f6f6f6;
"
>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top">&nbsp;</td>
<td
class="container"
style="
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
display: block;
margin: 0 auto;
max-width: 580px;
padding: 10px;
width: 580px;
"
>
<div
class="content"
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px"
>
<!-- START CENTERED WHITE CONTAINER -->
<span
class="preheader"
style="
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
></span>
<table
class="main"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
background: #ffffff;
border-radius: 5px;
"
>
<!-- START MAIN CONTENT AREA -->
<tr>
<td
class="wrapper"
style="
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
box-sizing: border-box;
padding: 20px;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top">
<p
style="
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
"
>
Hi there!
</p>
<p
style="
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
"
>
This is just an email to warn you that there was a
<strong>new successful login</strong> to your account in Padloc,
from {{ location }}.
</p>
<p
style="
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
"
>
If this was you, there's no action necessary, otherwise you might
want to make sure your trusted devices haven't been compromised, or
to remove potentially compromised devices from your trusted list
inside the app.
</p>
<p
style="
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
"
>
Have a great day!
</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%">
<table
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
>
<tr>
<td
class="content-block"
style="
font-family: sans-serif;
vertical-align: top;
padding-bottom: 10px;
padding-top: 10px;
font-size: 12px;
color: #999999;
text-align: center;
"
>
<span
class="apple-link"
style="color: #999999; font-size: 12px; text-align: center"
>This email was sent to you by Padloc (https://padloc.app). If you have any
questions, please don't hesitate to contact us at support@padloc.app!</span
>
<!--<br> Don't like these emails? <a href="" style="text-decoration: underline; color: #999999; font-size: 12px; text-align: center;">Unsubscribe</a>.-->
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,9 @@
Hi there!
This is just an email to warn you that there was a new successful login to your account in Padloc, from {{ location }}.
If this was you, there's no action necessary, otherwise you might want to make sure your trusted devices haven't been compromised, or to remove potentially compromised devices from your trusted list inside the app.
Have a great day!
This email was sent to you by Padloc (https://padloc.app). If you have any questions, please don't hesitate to contact us at support@padloc.app!

View File

@ -321,7 +321,7 @@ Cypress.Commands.add("v3_lock", () => {
// Click lock
cy.doWithin(["pl-app", "pl-menu"], () => cy.get("pl-icon[icon='lock'].tap").click({ force: true }));
cy.url().should("include", "/unlock");
cy.url({ timeout: 10000 }).should("include", "/unlock");
});
Cypress.Commands.add("v3_unlock", (email: string) => {
@ -337,5 +337,5 @@ Cypress.Commands.add("v3_unlock", (email: string) => {
cy.get("pl-loading-button#unlockButton").click({ force: true });
});
cy.url().should("include", "/items");
cy.url({ timeout: 10000 }).should("include", "/items");
});

View File

@ -347,6 +347,26 @@ export class LoginOrSignup extends StartForm {
this._loginPasswordInput.focus();
}
return;
case ErrorCode.INVALID_SESSION:
this._loginButton.stop();
await alert($l("We failed to verify your session. Please start over!"), {
type: "warning",
title: $l("Authentication Failed"),
});
try {
const pendingRequest = await this._getPendingAuth();
if (pendingRequest) {
this.router.setParams({ pendingAuth: undefined });
this.app.storage.delete(pendingRequest);
}
} catch (e) {}
await this.app.logout();
router.go("start", { email });
return;
case ErrorCode.NOT_FOUND:
this._loginButton.fail();
this._accountDoesntExist(email);

View File

@ -153,9 +153,9 @@ export class Audit extends StateMixin(Routing(View)) {
}
if (
!app.settings.securityReportWeak &&
!app.settings.securityReportReused &&
!app.settings.securityReportCompromised
!app.account?.settings.securityReport.weakPasswords &&
!app.account?.settings.securityReport.reusedPasswords &&
!app.account?.settings.securityReport.compromisedPaswords
) {
return html`
<div class="fullbleed centering double-padded text-centering vertical layout">
@ -175,9 +175,13 @@ export class Audit extends StateMixin(Routing(View)) {
const items = this._items;
return html`
<div class="counts">
${app.settings.securityReportWeak ? this._renderSection(items, AuditType.WeakPassword) : ""}
${app.settings.securityReportReused ? this._renderSection(items, AuditType.ReusedPassword) : ""}
${app.settings.securityReportCompromised
${app.account?.settings.securityReport.weakPasswords
? this._renderSection(items, AuditType.WeakPassword)
: ""}
${app.account?.settings.securityReport.reusedPasswords
? this._renderSection(items, AuditType.ReusedPassword)
: ""}
${app.account?.settings.securityReport.compromisedPaswords
? this._renderSection(items, AuditType.CompromisedPassword)
: ""}
</div>

View File

@ -147,17 +147,29 @@ export class SettingsSecurity extends StateMixin(Routing(LitElement)) {
}
}
private _updateSettings() {
app.setSettings({
private async _updateSettings() {
await app.setSettings({
autoLock: (this.renderRoot.querySelector("#autoLockButton") as ToggleButton).active,
autoLockDelay: (this.renderRoot.querySelector("#autoLockDelaySlider") as Slider).value,
securityReportWeak: (this.renderRoot.querySelector("#securityReportWeakToggle") as ToggleButton).active,
securityReportReused: (this.renderRoot.querySelector("#securityReportReusedToggle") as ToggleButton).active,
securityReportCompromised: (
this.renderRoot.querySelector("#securityReportCompromisedToggle") as ToggleButton
).active,
});
auditVaults();
await app.updateAccount(async (account) => {
account.settings.securityReport.weakPasswords = (
this.renderRoot.querySelector("#securityReportWeakToggle") as ToggleButton
).active;
account.settings.securityReport.reusedPasswords = (
this.renderRoot.querySelector("#securityReportReusedToggle") as ToggleButton
).active;
account.settings.securityReport.compromisedPaswords = (
this.renderRoot.querySelector("#securityReportCompromisedToggle") as ToggleButton
).active;
account.settings.notifications.failedLoginAttempts = (
this.renderRoot.querySelector("#failedLoginAttemptsNotificationsToggle") as ToggleButton
).active;
account.settings.notifications.newLogins = (
this.renderRoot.querySelector("#newLoginsNotificationsToggle") as ToggleButton
).active;
});
await auditVaults();
}
private async _addAuthenticator() {
@ -680,7 +692,7 @@ export class SettingsSecurity extends StateMixin(Routing(LitElement)) {
`;
}
private _rendersecurityReport() {
private _renderSecurityReport() {
return html`
<div class="box">
<h2 class="padded uppercase bg-dark border-bottom semibold">${$l("Security Report")}</h2>
@ -689,7 +701,7 @@ export class SettingsSecurity extends StateMixin(Routing(LitElement)) {
<pl-toggle-button
class="transparent"
id="securityReportWeakToggle"
.active=${app.settings.securityReportWeak}
.active=${app.account?.settings.securityReport.weakPasswords || false}
.label=${html`<div class="horizontal center-aligning spacing layout">
<pl-icon icon="weak"></pl-icon>
<div>${$l("Weak Passwords")}</div>
@ -703,7 +715,7 @@ export class SettingsSecurity extends StateMixin(Routing(LitElement)) {
<pl-toggle-button
class="transparent"
id="securityReportReusedToggle"
.active=${app.settings.securityReportReused}
.active=${app.account?.settings.securityReport.reusedPasswords || false}
.label=${html`<div class="horizontal center-aligning spacing layout">
<pl-icon icon="reused"></pl-icon>
<div>${$l("Reused Passwords")}</div>
@ -717,7 +729,7 @@ export class SettingsSecurity extends StateMixin(Routing(LitElement)) {
<pl-toggle-button
class="transparent"
id="securityReportCompromisedToggle"
.active=${app.settings.securityReportCompromised}
.active=${app.account?.settings.securityReport.compromisedPaswords || false}
.label=${html`<div class="horizontal center-aligning spacing layout">
<pl-icon icon="compromised"></pl-icon>
<div>${$l("Compromised Passwords")}</div>
@ -730,6 +742,42 @@ export class SettingsSecurity extends StateMixin(Routing(LitElement)) {
`;
}
private _renderEmailNotifications() {
return html`
<div class="box">
<h2 class="padded uppercase bg-dark border-bottom semibold">${$l("Email Notifications")}</h2>
<div>
<pl-toggle-button
class="transparent"
id="failedLoginAttemptsNotificationsToggle"
.active=${app.account?.settings.notifications.failedLoginAttempts || false}
.label=${html`<div class="horizontal center-aligning spacing layout">
<pl-icon icon="lock"></pl-icon>
<div>${$l("Failed Login Attempts")}</div>
</div>`}
reverse
>
</pl-toggle-button>
</div>
<div class="border-top">
<pl-toggle-button
class="transparent"
id="newLoginsNotificationsToggle"
.active=${app.account?.settings.notifications.newLogins || false}
.label=${html`<div class="horizontal center-aligning spacing layout">
<pl-icon icon="unlock"></pl-icon>
<div>${$l("New Logins (on new or untrusted devices)")}</div>
</div>`}
reverse
>
</pl-toggle-button>
</div>
</div>
`;
}
render() {
return html`
<div class="fullbleed vertical layout stretch background">
@ -783,7 +831,8 @@ export class SettingsSecurity extends StateMixin(Routing(LitElement)) {
</div>
${this._renderBiometricUnlock()} ${this._renderMFA()} ${this._renderSessions()}
${this._renderTrustedDevices()} ${this._rendersecurityReport()}
${this._renderTrustedDevices()} ${this._renderSecurityReport()}
${this._renderEmailNotifications()}
</div>
</pl-scroller>
</div>

View File

@ -215,6 +215,15 @@ export class Unlock extends StartForm {
if (e.code !== ErrorCode.DECRYPTION_FAILED) {
throw e;
}
if (this._failedCount > 3) {
await this.app.logout();
router.go("login");
alert($l("Failed to unlock too many times. You will have to login again."), {
title: $l("Failed To Unlock"),
type: "warning",
});
return;
}
this._errorMessage = $l("Wrong password! Please try again.");
this.rumble();

View File

@ -150,7 +150,7 @@ export async function auditVaults(
reusedPasswordItemIds.add(item.id);
}
if (app.settings.securityReportReused) {
if (app.account?.settings.securityReport.reusedPasswords) {
auditResults.push({
type: AuditType.ReusedPassword,
fieldIndex: passwordField.fieldIndex,
@ -160,7 +160,7 @@ export async function auditVaults(
vaultResultsFound = true;
}
if (app.settings.securityReportWeak) {
if (app.account?.settings.securityReport.weakPasswords) {
// Perform weak audit
const isThisPasswordWeak = await isPasswordWeak(passwordField.field.value);
if (isThisPasswordWeak) {
@ -179,7 +179,7 @@ export async function auditVaults(
}
}
if (app.settings.securityReportCompromised) {
if (app.account?.settings.securityReport.compromisedPaswords) {
// Perform compromised audit
const isPasswordCompromised = await hasPasswordBeenCompromised(passwordHash);
if (isPasswordCompromised) {

View File

@ -1,4 +1,4 @@
import { stringToBytes, concatBytes, Serializable, AsBytes, AsDate, AsSet, Exclude } from "./encoding";
import { stringToBytes, concatBytes, Serializable, AsBytes, AsDate, AsSet, Exclude, AsSerializable } from "./encoding";
import { RSAPublicKey, RSAPrivateKey, RSAKeyParams, HMACKey, HMACParams, HMACKeyParams } from "./crypto";
import { getCryptoProvider as getProvider } from "./platform";
import { Err, ErrorCode } from "./error";
@ -27,6 +27,25 @@ export class AccountSecrets extends Serializable {
favorites = new Set<VaultItemID>();
}
export class SecurityReportSettings extends Serializable {
weakPasswords = true;
reusedPasswords = true;
compromisedPaswords = true;
}
export class NotificationSettings extends Serializable {
failedLoginAttempts = true;
newLogins = true;
}
export class AccountSettings extends Serializable {
@AsSerializable(SecurityReportSettings)
securityReport = new SecurityReportSettings();
@AsSerializable(NotificationSettings)
notifications = new NotificationSettings();
}
export const ACCOUNT_NAME_MAX_LENGTH = 100;
export const ACCOUNT_EMAIL_MAX_LENGTH = 255;
@ -102,6 +121,10 @@ export class Account extends PBES2Container implements Storable {
@Exclude()
favorites = new Set<VaultItemID>();
/** Application Settings */
@AsSerializable(AccountSettings)
settings = new AccountSettings();
/**
* Whether or not this Account object is current "locked" or, in other words,
* whether the `privateKey` and `signingKey` properties have been decrypted.

View File

@ -57,12 +57,6 @@ export class Settings extends Serializable {
favicons = true;
/** Enable badge on web extension icon */
extensionBadge = true;
/** Enable checking for weak passwords */
securityReportWeak = true;
/** Enable checking for reused passwords */
securityReportReused = true;
/** Enable checking for compromised passwords */
securityReportCompromised = true;
/** Unmask Fields on hover */
unmaskFieldsOnHover = true;
}
@ -848,7 +842,7 @@ export class App {
/**
* Revokes the given [[Session]]
*/
async revokeSession({ id }: { id: SessionID }) {
async revokeSession(id: SessionID) {
await this.api.revokeSession(id);
await this.fetchAccount();
}

View File

@ -60,6 +60,24 @@ export class JoinOrgInviteCompletedMessage extends Message<{ orgName: string; op
}
}
export class FailedLoginAttemptMessage extends Message<{ location: string }> {
template = "failed-login-attempt";
get title() {
const appName = process.env.PL_APP_NAME;
return `${appName ? appName + " " : ""}Failed Login Attempt from ${this.data.location})`;
}
}
export class NewLoginMessage extends Message<{ location: string }> {
template = "new-login";
get title() {
const appName = process.env.PL_APP_NAME;
return `${appName ? appName + " " : ""}New Login from ${this.data.location})`;
}
}
export class PlainMessage extends Message<{ message: string }> {
template = "plain";

View File

@ -48,12 +48,14 @@ import {
JoinOrgInviteAcceptedMessage,
JoinOrgInviteCompletedMessage,
JoinOrgInviteMessage,
FailedLoginAttemptMessage,
NewLoginMessage,
Messenger,
} from "./messenger";
import { Server as SRPServer, SRPSession } from "./srp";
import { DeviceInfo, getCryptoProvider } from "./platform";
import { getIdFromEmail, uuid, removeTrailingSlash } from "./util";
import { loadLanguage } from "@padloc/locale/src/translate";
import { loadLanguage, translate as $l } from "@padloc/locale/src/translate";
import { Logger, VoidLogger } from "./logging";
import { PBES2Container } from "./container";
import { KeyStoreEntry } from "./key-store";
@ -551,6 +553,20 @@ export class Controller extends API {
});
}
private _buildLocationAndDeviceString(
locationData: { city?: string; country?: string } | undefined,
deviceInfo: DeviceInfo | undefined
) {
const location = locationData ? `${locationData.city}, ${locationData.country}` : $l("unknown location");
const device = deviceInfo?.description;
if (location && device) {
return `${device} in ${location}`;
}
return location;
}
async completeCreateSession({
accountId: account,
srpId,
@ -567,7 +583,7 @@ export class Controller extends API {
const srpState = auth.srpSessions.find((s) => s.id === srpId);
if (!srpState) {
throw new Err(ErrorCode.INVALID_CREDENTIALS, "No srp session with the given id found!");
throw new Err(ErrorCode.INVALID_SESSION, "No srp session with the given id found!");
}
const srp = new SRPServer(srpState);
@ -582,6 +598,30 @@ export class Controller extends API {
// authentication.
if (!(await getCryptoProvider().timingSafeEqual(M, srp.M1!))) {
this.log("account.createSession", { success: false });
++srpState.failedAttempts;
if (srpState.failedAttempts >= 5) {
if (this.context.device) {
try {
await this.removeTrustedDevice(this.context.device.id);
} catch (e) {}
}
// Delete pending SRP context
auth.srpSessions = auth.srpSessions.filter((s) => s.id !== srpState.id);
await this.storage.save(auth);
if (acc.settings.notifications.failedLoginAttempts) {
try {
const location = this._buildLocationAndDeviceString(this.context.location, this.context.device);
this.messenger.send(acc.email, new FailedLoginAttemptMessage({ location }));
} catch (e) {}
}
} else {
// Saves the updated failed attempts
await this.storage.save(auth);
}
throw new Err(ErrorCode.INVALID_CREDENTIALS);
}
@ -602,13 +642,21 @@ export class Controller extends API {
// Persist changes
await Promise.all([this.storage.save(session), this.storage.save(acc)]);
// Add device to trusted devices
if (
this.context.device &&
!auth.trustedDevices.some(({ id }) => id === this.context.device!.id) &&
addTrustedDevice
) {
auth.trustedDevices.push(this.context.device);
// Check if device isn't trusted
if (this.context.device && !auth.trustedDevices.some(({ id }) => id === this.context.device!.id)) {
// Add to trusted devices
if (addTrustedDevice) {
auth.trustedDevices.push(this.context.device);
}
// Send new login notification (it's a new or untrusted device)
if (acc.settings.notifications.newLogins) {
try {
const location = this._buildLocationAndDeviceString(this.context.location, this.context.device);
this.messenger.send(acc.email, new NewLoginMessage({ location }));
} catch (e) {}
}
}
await this.storage.save(auth);

View File

@ -139,6 +139,8 @@ export class SRPSession extends Serializable {
this.id = await uuid();
}
failedAttempts: number = 0;
@AsBigInteger()
x?: BigInteger;
@AsBigInteger()