Skip to content

Commit

Permalink
Added support for changing master password
Browse files Browse the repository at this point in the history
  • Loading branch information
bhatti committed Dec 17, 2023
1 parent 17a6533 commit 1b43996
Show file tree
Hide file tree
Showing 30 changed files with 737 additions and 73 deletions.
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,8 @@ Commands:
create-user

update-user

change-user-password

delete-user

Expand Down Expand Up @@ -1019,9 +1021,11 @@ You can update your user profile using CLI as follows:

You can update your user profile using REST APIs as follows:
```bash
./target/release/plexpass -j true --master-username charlie
--master-password *** --name "Charles" --email "[email protected]"
curl -k https://localhost:8443/api/v1/users/me
--header "Content-Type: application/json; charset=UTF-8"
--header "Authorization: Bearer $AUTH_TOKEN" -d '{"user_id"...}'
```

#### 11.5.3 Docker CLI

You can update your user profile using docker CLI as follows:
Expand All @@ -1040,6 +1044,38 @@ You can also update your user profile using web UI as displayed in following scr
The UI allows you to view/edit user profile, manage security-keys for multi-factor authentication and generate API tokens:
![](https://raw.githubusercontent.com/bhatti/PlexPass/main/docs/settings.png)

### 11.5b Update User Password

#### 11.5b.1 Command Line

You can update your user password using CLI as follows:
```bash
./target/release/plexpass -j true --master-username [email protected] --master-password ** \
change-user-password --new-password ** --confirm-new-password **
```
#### 11.5b.2 REST API

You can update your user password using REST APIs as follows:
```bash
curl -k https://localhost:8443/api/v1/users/me
--header "Content-Type: application/json; charset=UTF-8"
--header "Authorization: Bearer $AUTH_TOKEN" -d '{"old_password": "*", "new_password": "*", "confirm_new_password": "*"}'
```
#### 11.5b.3 Docker CLI

You can update your user profile using docker CLI as follows:
```bash
docker run -e DEVICE_PEPPER_KEY=$DEVICE_PEPPER_KEY
-e DATA_DIR=/data -v $PARENT_DIR/PlexPassData:/data
-j true --master-username [email protected] --master-password ** \
change-user-password --new-password ** --confirm-new-password **
```

#### 11.5b.4 Web UI

You can also update your user password from user settings using web UI as displayed in following screenshot:
![](https://raw.githubusercontent.com/bhatti/PlexPass/main/docs/change_password.png)

### 11.6 Creating Vaults

PlexPass automatically creates a few Vaults upon registration but you can create additional vaults as follows:
Expand Down
95 changes: 71 additions & 24 deletions assets/javascript/plexpass.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ async function viewAccount(id) {
<tr>
<td><strong>Password:</strong> </td>
<td class="d-flex">
<input type="password" class="form-control" id="accountPassword" name="accountPassword" value="${account.password || ''}" disabled>
<input type="password" class="form-control" id="accountPassword" name="accountPassword" value="" disabled>
&nbsp;
<button id="viewPasswordButton" class="btn btn-outline-info" onclick="togglePasswordVisibility()">Show</button>
&nbsp;
<button class="btn btn-outline-warning" onclick="copyToClipboard('${account.password}')">Copy</button>
<button id="copyPasswordButton" class="btn btn-outline-warning">Copy</button>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -167,6 +167,10 @@ async function viewAccount(id) {
</tr>
</table>
`;
document.getElementById('accountPassword').value = account.password || '';
document.getElementById('copyPasswordButton').onclick = function () {
copyToClipboard(account.password);
};
// Show modal
const viewModalElem = document.getElementById('viewAccountModal');
const viewModal = new bootstrap.Modal(viewModalElem);
Expand Down Expand Up @@ -436,7 +440,7 @@ async function showAccountForm(account) {
</div>
<div class="form-group mb-3">
<label for="password" class="form-label">Password:</label>
<input type="password" class="form-control" id="password" name="password" value="${account.password || ''}">
<input type="password" class="form-control" id="password" name="password">
</div>
<div class="form-group mb-3">
<label for="website_url" class="form-label">Website URL:</label>
Expand Down Expand Up @@ -514,6 +518,7 @@ async function showAccountForm(account) {
</div>
`;

document.getElementById('password').value = account.password || '';
const customFieldsContainer = document.getElementById('customFieldsEdit');
if (account.form_fields) {
for (const [key, value] of Object.entries(account.form_fields)) {
Expand Down Expand Up @@ -624,7 +629,8 @@ async function doFetch(path, headers) {
const response = await fetch(path, headers);
if (response.status === 401 || response.headers.get('X-Signin') === 'required') {
window.location.href = '/ui/signin';
throw new Error(`Signin required Status: ${response.status} ${response.statusText}`);
const text = await response.text();
throw new Error(`Signin required Status: ${response.status} ${response.statusText} ${text}`);
}
return response;
}
Expand All @@ -639,7 +645,8 @@ async function handleShareUnShareVault(vaultId) {
if (response.status === 409) {
alert(`You have already ${shareUnshare}d vault with the ${username}`);
} else {
alert(`Could not ${shareUnshare} vault: ${response.status} ${response.statusText}`);
const text = await response.text();
alert(`Could not ${shareUnshare} vault: ${response.status} ${response.statusText} ${text}`);
}
return;
}
Expand Down Expand Up @@ -667,8 +674,9 @@ async function handleShareAccount(vaultId) {
method: 'POST',
});
if (!response.ok) {
alert(`Could not share account: ${response.status} ${response.statusText}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
const text = await response.text();
alert(`Could not share account: ${response.status} ${response.statusText} ${text}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText} ${text}`);
}
const viewModal = bootstrap.Modal.getInstance(document.getElementById('shareAccountModal'));
await viewModal.hide();
Expand Down Expand Up @@ -726,12 +734,13 @@ async function handleImportAccounts(vaultId) {
body: formData
});
if (!response.ok) {
const text = await response.text();
if (password) {
alert(`Could not import accounts, please verify password: ${response.status} ${response.statusText}`);
alert(`Could not import accounts, please verify password: ${response.status} ${response.statusText} ${text}`);
} else {
alert(`Could not import accounts, please try again: ${response.status} ${response.statusText}`);
alert(`Could not import accounts, please try again: ${response.status} ${response.statusText} ${text}`);
}
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText} ${text}`);
}

let reader = response.body.getReader();
Expand Down Expand Up @@ -790,8 +799,9 @@ async function deleteAccount(id) {
}
);
if (!response.ok) {
alert(`Could not delete account ${response.status} ${response.statusText}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
const text = await response.text();
alert(`Could not delete account ${response.status} ${response.statusText} ${text}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText} ${text}`);
} else {
showToast('Data deleted successfully!', () => {
location.reload();
Expand All @@ -816,6 +826,33 @@ async function handleSaveAccount(form) {
}
}

async function changePassword() {
const form = document.forms['changePasswordForm'];
const formData = new FormData(form);
const newPassword = document.getElementById('newPassword').value.trim();
const confirmNewPassword = document.getElementById('confirmNewPassword').value.trim();
if (newPassword !== confirmNewPassword) {
alert("New password didn't match confirm password");
return;
}
const response = await doFetch("/ui/users/change_password", {
method: 'POST',
body: formData,
});
if (!response.ok) {
const text = await response.text();
alert(`Could not change password: ${text}`);
console.error(`HTTP error! Status: ${response.status} status ${response.statusText} ${text}`);
return;
}
const viewModal = bootstrap.Modal.getInstance(document.getElementById('changePasswordModal'));
await viewModal.hide();
alert('Your password is changed and all data is now encrypted with new password so you will need to sign-in with new password!');
window.location.href = '/ui/signin';
return true;
}


function togglePasswordVisibility() {
const passwordButtonSpan = document.getElementById('viewPasswordButton');
const accountPassword = document.getElementById('accountPassword');
Expand All @@ -837,7 +874,8 @@ async function fetchAccount(id) {
}
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
const text = await response.text();
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText} ${text}`);
}
try {
return await response.json();
Expand All @@ -852,7 +890,8 @@ async function postData(path, data) {
body: data,
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
const text = await response.text();
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText} ${text}`);
}
return true; //await response.json();
}
Expand Down Expand Up @@ -1063,7 +1102,8 @@ async function scheduleAnalysis() {
if (response.ok) {
showToast('Scheduled password analysis!');
} else {
alert(`Could not schedule password analysis: ${response.status} ${response.statusText}`);
const text = await response.text();
alert(`Could not schedule password analysis: ${response.status} ${response.statusText} ${text}`);
}
}

Expand Down Expand Up @@ -1182,8 +1222,9 @@ async function saveCategory() {
},
})
if (!response.ok) {
alert(`Could not add category ${response.status} ${response.statusText}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
const text = await response.text();
alert(`Could not add category ${response.status} ${response.statusText} ${text}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText} ${text}`);
}
const viewModal = bootstrap.Modal.getInstance(document.getElementById('categoryModal'));
await viewModal.hide();
Expand All @@ -1203,8 +1244,9 @@ async function deleteCategory(name) {
},
})
if (!response.ok) {
alert(`Could not delete category ${response.status} ${response.statusText}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
const text = await response.text();
alert(`Could not delete category ${response.status} ${response.statusText} ${text}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText} ${text}`);
} else {
showToast('Category deleted successfully!', () => {
location.reload();
Expand Down Expand Up @@ -1246,8 +1288,9 @@ async function removeMFAKey(id) {
location.reload();
})
} else {
alert(`Could not remove MFA keys ${response.status} ${response.statusText}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
const text = await response.text();
alert(`Could not remove MFA keys ${response.status} ${response.statusText} ${text}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText} ${text}`);
}
} catch (error) {
console.error('Error removing MFA key:', error);
Expand Down Expand Up @@ -1395,6 +1438,7 @@ async function registerMFAKey() {
location.reload();
};
} catch (err) {
console.trace();
console.error('Error during registration:', err);
}
}
Expand Down Expand Up @@ -1452,7 +1496,8 @@ async function updateVaultDetails(vaultId, version) {
location.reload();
});
} else {
throw new Error(`Failed to save the vault: ${response.status} ${response.statusText}`);
const text = await response.text();
throw new Error(`Failed to save the vault: ${response.status} ${response.statusText} ${text}`);
}
} catch (e) {
console.error('Failed to save the vault', e);
Expand All @@ -1469,8 +1514,9 @@ async function deleteVault(vaultId, title) {
},
})
if (!response.ok) {
alert(`Could not delete vault ${response.status} ${response.statusText}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
const text = await response.text();
alert(`Could not delete vault ${response.status} ${response.statusText} ${text}`);
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText} ${text}`);
} else {
showToast('Vault deleted successfully!', () => {
location.reload();
Expand All @@ -1491,6 +1537,7 @@ async function checkSession() {
}
} catch (error) {
console.error('Session check failed:', error);
window.location.href = '/ui/signin';
}
}

Expand Down
Binary file added docs/change_password.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/mfa_codes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/mfa_diag1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/mfa_diag2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions functional_tests/00_local_start_server.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/bin/bash -e
#export DOMAIN=plexpass
source env.sh
#export RUST_LOG="actix_web=trace"
#export RUST_LOG="actix_web=trace"
Expand Down
45 changes: 44 additions & 1 deletion functional_tests/01_user_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ def test_00_signin_without_signup(self):
resp = requests.post(SERVER + '/api/v1/auth/signin', json = data, headers = headers, verify = False)
self.assertTrue(resp.status_code == 200 or resp.status_code == 401) # should throw 401 if user doesn't exist

def test_00_signup_george(self):
headers = {
'Content-Type': 'application/json',
}
data = {'username': '[email protected]', 'master_password': '[email protected]$Goat551'}
resp = requests.post(SERVER + '/api/v1/auth/signup', json = data, headers = headers, verify = False)
self.assertTrue(resp.status_code == 200 or resp.status_code == 409)

def test_01_signup_alice(self):
headers = {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -66,7 +74,7 @@ def test_05_update_user(self):
'Authorization': 'Bearer ' + JWT_TOKEN,
}

data = {'user_id':USER_ID, 'version':VERSION, 'username': '[email protected]', 'name': 'Bill', 'email': 'bill@nowhere', 'icon': 'stuff'}
data = {'user_id':USER_ID, 'version':VERSION, 'username': '[email protected]', 'name': 'Bill', 'email': 'bill@nowhere'}
resp = requests.put(SERVER + '/api/v1/users/' + USER_ID, json = data, headers = headers, verify = False)
self.assertEqual(200, resp.status_code)

Expand All @@ -89,5 +97,40 @@ def test_07_get_user_without_token(self):
resp = requests.get(SERVER + '/api/v1/users/' + USER_ID, headers = headers, verify = False)
self.assertEqual(401, resp.status_code)

def test_08_signin_george(self):
global USER_ID
global JWT_TOKEN
headers = {
'Content-Type': 'application/json',
}
data = {'username': '[email protected]', 'master_password': '[email protected]$Goat551'}
resp = requests.post(SERVER + '/api/v1/auth/signin', json = data, headers = headers, verify = False)
self.assertEqual(200, resp.status_code)
USER_ID = json.loads(resp.text)['user_id']
JWT_TOKEN = resp.headers.get('access_token')

def test_09_change_password(self):
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + JWT_TOKEN,
}

data = {'old_password': '[email protected]$Goat551', 'new_password': '[email protected]$Goat5511', 'confirm_new_password': '[email protected]$Goat5511'}
resp = requests.put(SERVER + '/api/v1/users/' + USER_ID + '/change_password', json = data, headers = headers, verify = False)
self.assertEqual(200, resp.status_code)

def test_10_signin_george_with_new_password(self):
global USER_ID
global JWT_TOKEN
headers = {
'Content-Type': 'application/json',
}
data = {'username': '[email protected]', 'master_password': '[email protected]$Goat5511'}
resp = requests.post(SERVER + '/api/v1/auth/signin', json = data, headers = headers, verify = False)
self.assertEqual(200, resp.status_code)
USER_ID = json.loads(resp.text)['user_id']
JWT_TOKEN = resp.headers.get('access_token')


if __name__ == '__main__':
unittest.main()
1 change: 1 addition & 0 deletions migrations/2023-09-03-211658_crypto_keys/up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS crypto_keys
encrypted_private_key VARCHAR(100) NOT NULL,
encrypted_symmetric_key VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (crypto_key_id)
);

Expand Down
3 changes: 3 additions & 0 deletions resources/en-US/errors.ftl
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
auth-error = We could not validate your credentials, please verify them if you already have an account or Sign up as a new user.
weak-master-password = Your master password with {$info}. Please choose a strong password with a minimum of 12 characters, containing uppercase and lowercase letters, numbers, and special symbols such as `{ $sample_password }`.
master-confirm-mismatch = Your master-password didn't match confirmed master-password, please confirm again.
old-master-mismatch = Your old master-password didn't match the password you provided, please confirm again.
new-master-confirm-mismatch = Your new master-password didn't match confirmed new master-password, please confirm again.
same-new-master-password = Your new master-password is same as old master-password, please use new password.
email-compromise-error = failed to check email for compromise: {$err}.
short-secret-error = secret length {$len} is too small.
user-id-mismatch-error = user_id in context {$id1} didn't match user_id {$id2} in the request.
Expand Down
Loading

0 comments on commit 1b43996

Please sign in to comment.