Skip to content

Commit

Permalink
feat: calculate password strength
Browse files Browse the repository at this point in the history
  • Loading branch information
martinkaintas authored and peronczyk committed Oct 11, 2024
1 parent 657802a commit 3c738e2
Show file tree
Hide file tree
Showing 17 changed files with 231 additions and 48 deletions.
6 changes: 6 additions & 0 deletions src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ import { updateDynamicRules } from './redirectRule';
*/
function handleMessage(msg: IPopupMessageData, _: any, sendResponse: Function) {
if (msg.target === 'background') {
// Handle session expiration independently because params are not set
if (msg.method === 'setSessionExpires') {
// @ts-expect-error session storage is not defined
browser.storage.session.set({ sessionExpires: msg.payload });
return false;
}
const {
aepp,
id,
Expand Down
25 changes: 24 additions & 1 deletion src/composables/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@ import {
ACCOUNT_TYPES,
MODAL_SET_PASSWORD,
MODAL_PASSWORD_LOGIN,
IS_EXTENSION,
} from '@/constants';
import {
createCallbackRegistry,
decrypt,
encrypt,
endSession,
excludeFalsy,
generateKey,
getSessionKey,
prepareAccountSelectOptions,
startSession,
watchUntilTruthy,
} from '@/utils';
import { ProtocolAdapterFactory } from '@/lib/ProtocolAdapterFactory';
Expand All @@ -39,6 +43,7 @@ import migrateMnemonicCordovaToIonic from '@/migrations/008-mnemonic-cordova-to-
import { WalletStorage } from '@/lib/WalletStorage';
import { useStorageRef } from './storageRef';
import { useModals } from './modals';
import { useUi } from './ui';

let composableInitialized = false;

Expand Down Expand Up @@ -174,6 +179,8 @@ const protocolsInUse = computed(
* The wallets's data is created in fly with the use of computed properties.
*/
export function useAccounts() {
const { secureLoginTimeout, setLoaderVisible } = useUi();

function getAccountByAddress(address: AccountAddress): IAccount | undefined {
return accounts.value.find((acc) => acc.address === address);
}
Expand Down Expand Up @@ -238,12 +245,28 @@ export function useAccounts() {
*/
function setPasswordKey(key: IKey | null) {
passwordKey.value = key;
if (IS_EXTENSION) {
if (key) {
startSession(key, secureLoginTimeout.value);
} else {
endSession();
}
}
}

async function openLoginModal() {
setLoaderVisible(true);
const sessionKey = await getSessionKey();
if (sessionKey) {
setPasswordKey(sessionKey);
setLoaderVisible(false);
return;
}
setLoaderVisible(false);

const { openModal } = useModals();

await openModal<string>(MODAL_PASSWORD_LOGIN);
await openModal(MODAL_PASSWORD_LOGIN);
if (!passwordKey.value) {
throw new Error('passwordKey was not set after login.');
}
Expand Down
18 changes: 15 additions & 3 deletions src/composables/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import {
} from 'vue';

import { tg as t } from '@/popup/plugins/i18n';
import { IS_MOBILE_APP, MODAL_ENABLE_BIOMETRIC_LOGIN, MODAL_SECURE_LOGIN } from '@/constants';
import { authenticateWithPassword, watchUntilTruthy } from '@/utils';
import {
IS_EXTENSION,
IS_MOBILE_APP,
MODAL_ENABLE_BIOMETRIC_LOGIN,
MODAL_SECURE_LOGIN,
} from '@/constants';
import { authenticateWithPassword, getSessionKey, watchUntilTruthy } from '@/utils';
import { useUi } from './ui';
import { useModals } from './modals';
import { useAccounts } from './accounts';
Expand Down Expand Up @@ -85,7 +90,13 @@ export const useAuth = createCustomScopedComposable(() => {
return Promise.resolve();
}

function logout() {
async function logout() {
if (IS_EXTENSION) {
const sessionKey = await getSessionKey();
if (sessionKey) {
return;
}
}
setPasswordKey(null);
isAuthenticated.value = false;
}
Expand Down Expand Up @@ -136,6 +147,7 @@ export const useAuth = createCustomScopedComposable(() => {
await openSecureLoginModal();
}
} else if (!isAuthenticated.value) {
logout();
await openSecureLoginModal();
}
}
Expand Down
26 changes: 12 additions & 14 deletions src/composables/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,13 @@ import {
} from 'vue';
import { RouteLocationRaw } from 'vue-router';
import { AUTHENTICATION_TIMEOUTS, IS_MOBILE_APP, STORAGE_KEYS } from '@/constants';
import { endSession } from '@/utils';
import { ROUTE_ACCOUNT } from '@/popup/router/routeNames';
import migrateHiddenCardsVuexToComposable from '@/migrations/004-hidden-cards-vuex-to-composables';
import migrateOtherSettingsVuexToComposable from '@/migrations/005-other-settings-vuex-to-composables';
import { IOtherSettings } from '@/types';
import { useStorageRef } from './storageRef';

export interface IOtherSettings {
isSeedBackedUp?: boolean;
saveErrorLog?: boolean;
isBiometricLoginEnabled?: boolean;
secureLoginTimeout?: number;
}

/** Control the route that would be visible after opening the extension. */
const homeRouteName = ref(ROUTE_ACCOUNT);

Expand Down Expand Up @@ -47,7 +42,9 @@ const hiddenCards = useStorageRef<string[]>(
},
);
const otherSettings = useStorageRef<IOtherSettings>(
{},
{
secureLoginTimeout: IS_MOBILE_APP ? AUTHENTICATION_TIMEOUTS[0] : AUTHENTICATION_TIMEOUTS[5],
},
STORAGE_KEYS.otherSettings,
{
migrations: [
Expand All @@ -59,11 +56,7 @@ const otherSettings = useStorageRef<IOtherSettings>(
const isSeedBackedUp = computed(() => !!otherSettings.value.isSeedBackedUp);
const saveErrorLog = computed(() => !!otherSettings.value.saveErrorLog);
const isBiometricLoginEnabled = computed(() => !!otherSettings.value.isBiometricLoginEnabled);
const secureLoginTimeout = computed(() => otherSettings.value.secureLoginTimeout
?? ((IS_MOBILE_APP)
? AUTHENTICATION_TIMEOUTS[0]
: AUTHENTICATION_TIMEOUTS[5]
));
const secureLoginTimeout = computed(() => otherSettings.value.secureLoginTimeout);

export function useUi() {
function setHomeRouteName(routeName: string, onChangeCallback?: () => any) {
Expand Down Expand Up @@ -109,6 +102,9 @@ export function useUi() {
}

function setSecureLoginTimeout(ms: number) {
if (ms === 0) {
endSession();
}
otherSettings.value.secureLoginTimeout = ms;
}

Expand All @@ -135,7 +131,9 @@ export function useUi() {

function resetUiSettings() {
hiddenCards.value = [];
otherSettings.value = {};
otherSettings.value = {
secureLoginTimeout: IS_MOBILE_APP ? AUTHENTICATION_TIMEOUTS[0] : AUTHENTICATION_TIMEOUTS[5],
};
}

return {
Expand Down
8 changes: 8 additions & 0 deletions src/constants/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const TX_DIRECTION = {
} as const;

export const CONNECTION_TYPES = {
SESSION: 'SESSION',
POPUP: 'POPUP',
OTHER: 'OTHER',
};
Expand Down Expand Up @@ -379,6 +380,7 @@ export const POPUP_METHODS = {
reload: 'reload',
paste: 'paste',
checkHasAccount: 'checkHasAccount', // TODO check if still used
setSessionExpires: 'setSessionExpires',
} as const;

export const AIRGAP_SIGNED_TRANSACTION_MESSAGE_TYPE = 'airgap-signed-transaction';
Expand Down Expand Up @@ -478,3 +480,9 @@ export const AUTHENTICATION_TIMEOUTS = {
15: 900000,
30: 1800000,
} as const;

export const PASSWORD_STRENGTH = {
weak: 'Weak',
medium: 'Medium',
strong: 'Strong',
} as const;
5 changes: 3 additions & 2 deletions src/migrations/005-other-settings-vuex-to-composables.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Migration } from '@/types';
import type { IOtherSettings } from '@/composables';
import type { Migration, IOtherSettings } from '@/types';
import { AUTHENTICATION_TIMEOUTS, IS_MOBILE_APP } from '@/constants';
import { collectVuexState } from './migrationHelpers';

const migration: Migration<IOtherSettings> = async (restoredValue: IOtherSettings) => {
Expand All @@ -8,6 +8,7 @@ const migration: Migration<IOtherSettings> = async (restoredValue: IOtherSetting
return {
isSeedBackedUp: backedUpSeed,
saveErrorLog,
secureLoginTimeout: IS_MOBILE_APP ? AUTHENTICATION_TIMEOUTS[0] : AUTHENTICATION_TIMEOUTS[5],
};
}
return restoredValue;
Expand Down
27 changes: 25 additions & 2 deletions src/offscreen/offscreen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import '@/lib/initPolyfills';
import '@/protocols/registerAdapters';
import { IPopupMessageData } from '@/types';
import { IS_FIREFOX, POPUP_METHODS, UNFINISHED_FEATURES } from '@/constants';
import { IOtherSettings, IPopupMessageData } from '@/types';
import {
CONNECTION_TYPES,
IS_FIREFOX,
POPUP_METHODS,
STORAGE_KEYS,
UNFINISHED_FEATURES,
} from '@/constants';
import { WalletStorage } from '@/lib/WalletStorage';
import { useWalletConnect } from '@/composables';
import * as wallet from './wallet';
import { useAccounts } from '../composables/accounts';
Expand All @@ -13,6 +20,22 @@ if (IS_FIREFOX) {
browser.runtime.onInstalled.addListener(updateDynamicRules);
}

browser.runtime.onConnect.addListener((port) => {
if (port.name === CONNECTION_TYPES.SESSION) {
port.onDisconnect.addListener(async () => {
const settings: IOtherSettings | null = await WalletStorage.get(STORAGE_KEYS.otherSettings);
const sessionExpires = Date.now() + (settings?.secureLoginTimeout ?? 0);

// browser.storage is not available in offscreen tab
browser.runtime.sendMessage<IPopupMessageData>({
target: 'background',
method: POPUP_METHODS.setSessionExpires,
payload: sessionExpires,
});
});
}
});

browser.runtime.onMessage.addListener(async ({ method }: IPopupMessageData) => {
if (method === POPUP_METHODS.reload) {
wallet.disconnect();
Expand Down
15 changes: 4 additions & 11 deletions src/popup/components/InputPassword.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@

<script>
import { computed, defineComponent, ref } from 'vue';
import { checkPasswordStrength } from '@/utils';
import InputField from './InputField.vue';
import EyeIcon from '../../icons/eye-open.svg?vue-component';
Expand All @@ -43,17 +46,7 @@ export default defineComponent({
setup(props) {
const isPasswordVisible = ref(false);
// TODO pin: password strength calculation
const passwordStrength = computed(() => {
switch (true) {
case props.modelValue.length < 8:
return 'Weak';
case props.modelValue.length < 12:
return 'Medium';
default:
return 'Strong';
}
});
const passwordStrength = computed(() => checkPasswordStrength(props.modelValue));
function toggleVisibility() {
isPasswordVisible.value = !isPasswordVisible.value;
Expand Down
8 changes: 3 additions & 5 deletions src/popup/components/Modals/SetPassword.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<div class="text-description">
<p
v-text="isRestoredWallet
? $t('pages.secureLogin.setPassword.textRestoredWallet-1', [extensionVersion])
? $t('pages.secureLogin.setPassword.textRestoredWallet-1')
: $t('pages.secureLogin.setPassword.text-2')"
/>
<p
Expand All @@ -44,7 +44,7 @@
:validate-on-blur="true"
:validate-on-model-update="!!errors.password"
:rules="{
password_min_len: 8,
password_min_len: 4,
}"
>
<InputPassword
Expand Down Expand Up @@ -74,7 +74,7 @@
<InputPassword
v-bind="field"
v-model="confirmPassword"
data-cy="confirmPassword"
data-cy="confirm-password"
class="password-input"
:placeholder="$t('pages.secureLogin.setPassword.confirmPlaceholder')"
:label="$t('pages.secureLogin.setPassword.confirmLabel')"
Expand Down Expand Up @@ -155,8 +155,6 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@use '@/styles/variables' as *;
.set-password {
.info {
margin-top: 8px;
Expand Down
2 changes: 1 addition & 1 deletion src/popup/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@
"title": "Set a password",
"text": "to securely access your wallet",
"text-2": "Choose a strong password and make sure you remember it or back it up safely.",
"textRestoredWallet-1": "Superhero Wallet has been updated to v.{0} introducing increased level of security.",
"textRestoredWallet-1": "Superhero Wallet has been updated introducing increased level of security.",
"textRestoredWallet-2": "You need to set a password to continue using your wallet. Chose a strong one and make sure you remember it or back it up safely.",
"passwordLabel": "Password",
"passwordPlaceholder": "Enter strong password",
Expand Down
18 changes: 12 additions & 6 deletions src/popup/pages/SecureLoginSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,20 @@
<hr>
<div class="options">
<div class="options-info">
<span class="options-label" v-text="$t('pages.secureLogin.changePassword.title')" />
<span class="options-description" v-text="$t('pages.secureLogin.changePassword.description')" />
<span
class="options-label"
v-text="$t('pages.secureLogin.changePassword.title')"
/>
<span
class="options-description"
v-text="$t('pages.secureLogin.changePassword.description')"
/>
</div>
<div class="inputs">
<div class="current-password">
<InputPassword
v-model="currentPassword"
data-cy="currentPassword"
data-cy="current-password"
:placeholder="$t('pages.secureLogin.changePassword.currentPasswordPlaceholder')"
:label="$t('pages.secureLogin.changePassword.currentPassword')"
:message="isAuthFailed ? $t('pages.secureLogin.login.error') : null"
Expand All @@ -72,13 +78,13 @@
:validate-on-blur="true"
:validate-on-model-update="!!errors.password"
:rules="{
password_min_len: 8,
password_min_len: 4,
}"
>
<InputPassword
v-bind="field"
v-model="newPassword"
data-cy="newPassword"
data-cy="new-password"
:placeholder="$t('pages.secureLogin.changePassword.newPasswordPlaceholder')"
:label="$t('pages.secureLogin.changePassword.newPassword')"
:message="errorMessage ?? errors.confirmNewPassword"
Expand All @@ -96,7 +102,7 @@
<InputPassword
v-bind="field"
v-model="confirmNewPassword"
data-cy="confirmNewPassword"
data-cy="confirm-new-password"
:placeholder="$t('pages.secureLogin.setPassword.confirmPlaceholder')"
:label="$t('pages.secureLogin.changePassword.confirmNewPassword')"
:message="errorMessage"
Expand Down
Loading

0 comments on commit 3c738e2

Please sign in to comment.