Compare commits
9 Commits
main
...
feature/em
Author | SHA1 | Date |
---|---|---|
Bruno Bernardino | 9b66955962 | |
Bruno Bernardino | 4b46a5f766 | |
Bruno Bernardino | ed550d71a5 | |
Bruno Bernardino | e0933dbfc1 | |
Bruno Bernardino | df18e71e6a | |
Bruno Bernardino | 8d0a255ce7 | |
Bruno Bernardino | 052477b6c9 | |
Bruno Bernardino | d351f80ac6 | |
Bruno Bernardino | d1f3295879 |
42
CHANGELOG.md
42
CHANGELOG.md
|
@ -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).
|
||||
|
|
|
@ -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"> </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"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -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!
|
|
@ -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"> </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"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -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!
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -139,6 +139,8 @@ export class SRPSession extends Serializable {
|
|||
this.id = await uuid();
|
||||
}
|
||||
|
||||
failedAttempts: number = 0;
|
||||
|
||||
@AsBigInteger()
|
||||
x?: BigInteger;
|
||||
@AsBigInteger()
|
||||
|
|
Loading…
Reference in New Issue