clear-wallet/src/views/SettingsTab.vue

690 lines
22 KiB
Vue

<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Settings</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-accordion-group :value="defaultAccordionOpen" v-if="!loading">
<ion-accordion value="1">
<ion-item slot="header" color="light">
<ion-label>Security</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
<ion-list>
<ion-item v-if="noAccounts"
>You need at least one account to touch this settings</ion-item
>
<ion-list :disabled="noAccounts">
<ion-item>
<ion-label>Enable Storage Encryption</ion-label>
<ion-toggle
:key="updateKey"
@ion-change="changeEncryption"
slot="end"
:checked="settings.s.enableStorageEnctyption"
></ion-toggle>
</ion-item>
<ion-item>
This will require to input an encrypto key when storage is locked.
</ion-item>
</ion-list>
<ion-item :disabled="!settings.s.enableStorageEnctyption">
<ion-label>Enable Auto Lock</ion-label>
<ion-toggle
:key="updateKey"
@ion-change="changeAutoLock"
slot="end"
:checked="settings.s.lockOutEnabled"
></ion-toggle>
</ion-item>
<ion-list>
<ion-item
:disabled="
!settings.s.enableStorageEnctyption || !settings.s.lockOutEnabled
"
>
<ion-label>Auto-lock Period: (2-120) minutes</ion-label>
</ion-item>
<ion-item
:disabled="
!settings.s.enableStorageEnctyption || !settings.s.lockOutEnabled
"
>
<ion-input
:key="updateKey"
v-model="settings.s.lockOutPeriod"
type="number"
></ion-input>
</ion-item>
<ion-item
:disabled="
!settings.s.enableStorageEnctyption || !settings.s.lockOutEnabled
"
>
<ion-button @click="setTime">Set Auto-lock</ion-button>
</ion-item>
</ion-list>
<ion-list>
<ion-item>
<ion-label>Permanent Lock</ion-label>
<ion-toggle
@ion-change="changePermaLock"
:key="updateKey"
slot="end"
:disabled="!settings.s.enableStorageEnctyption"
:checked="settings.s.encryptAfterEveryTx"
></ion-toggle>
</ion-item>
<ion-item
>Will require decrypt pass before any sign or transaction</ion-item
>
</ion-list>
</ion-list>
</div>
</ion-accordion>
<ion-accordion value="2">
<ion-item slot="header" color="light">
<ion-label>Theme</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
<ion-list>
<ion-radio-group :value="radioTheme">
<ion-item>
<ion-radio slot="start" value="system" @click="changeTheme('system')" />
<ion-label>System Default</ion-label>
</ion-item>
<ion-item>
<ion-radio slot="start" value="dark" @click="changeTheme('dark')" />
<ion-label>Dark</ion-label>
</ion-item>
<ion-item>
<ion-radio slot="start" value="light" @click="changeTheme('light')" />
<ion-label>Light</ion-label>
</ion-item>
</ion-radio-group>
</ion-list>
</div>
</ion-accordion>
<ion-accordion value="3">
<ion-item slot="header" color="light">
<ion-label>About</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
<p>
Clear EVM Wallet (CLW) is a fully open-source wallet built with Vue, Ionic,
and Ethers.
</p>
<p>
It emulates Metamask Wallet and can be used as a drop-in replacement, right
now if you have both extensions, CLW will overwrite Metamask.
</p>
<p>
Main philosophy of the wallet is: no trackers, full control, export/import
JSONs with accounts, fast generate new accounts, and wipe everything with
one click.
</p>
<p>
Github Repo:
<a href="#" @click="openTab('https://github.com/andrei0x309/clear-wallet')"
>LINK</a
>
</p>
<br />
<p style="margin-bottom: 0.2rem">Places you can check me out:</p>
<p>
Github andrei0x309 -
<a href="#" @click="openTab('https://github.com/andrei0x309')">LINK</a>
</p>
<p>
Mirror Profile
<a href="#" @click="openTab('https://mirror.xyz/andrei0x309.eth')">LINK</a>
</p>
<p>
Blog Flashsoft
<a href="#" @click="openTab('https://blog.flashsoft.eu')">LINK</a>
</p>
<p>
Crypto-Leftists Discord
<a href="#" @click="openTab('https://discord.gg/gzA4bTCdhb')">LINK</a>
</p>
</div>
</ion-accordion>
<ion-accordion value="4">
<ion-item slot="header" color="light">
<ion-label> Import / Export Accounts</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
<ion-item>
<ion-label>Import Additional Accounts</ion-label>
<input ref="importFile" type="file" accept=".json" />
<ion-button color="warning" @click="importAcc">Import</ion-button>
</ion-item>
<ion-item>
<ion-label>Export All Accounts</ion-label>
<ion-button color="warning" @click="exportAcc">Export</ion-button>
</ion-item>
</div>
</ion-accordion>
<ion-accordion value="5">
<ion-item slot="header" color="light">
<ion-label>Danger</ion-label>
</ion-item>
<div class="ion-padding" slot="content">
<ion-item>
<ion-label>WIPE All DATA</ion-label>
<ion-button color="danger" @click="wipeStorage">PERMA WIPE</ion-button>
</ion-item>
</div>
</ion-accordion>
</ion-accordion-group>
<ion-toast
:is-open="toastState"
@didDismiss="toastState = false"
:message="toastMsg"
:duration="1500"
></ion-toast>
<ion-loading
:is-open="loading"
cssClass="my-custom-class"
message="Please wait..."
:duration="4000"
@didDismiss="loading = false"
>
</ion-loading>
<ion-modal
:is-open="mpModal"
@did-dismiss="
mpModal = false;
modalDismiss();
"
>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button
@click="
modalGetPassword?.reject
? (() => {
modalGetPassword.reject();
modalGetPassword = null;
})()
: (mpModal = false)
"
>Close</ion-button
>
</ion-buttons>
<ion-title v-if="!settings.s.enableStorageEnctyption"
>Create Encryption Password</ion-title
>
<ion-title v-else>Enter Encryption Password</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list v-if="settings.s.enableStorageEnctyption">
<ion-item>
<ion-label>Old Password</ion-label>
</ion-item>
<ion-item>
<ion-input v-model="mpPass" type="password"></ion-input>
</ion-item>
</ion-list>
<div v-else>
<ion-list>
<ion-item>
<ion-label>New Password</ion-label>
</ion-item>
<ion-item>
<ion-input v-model="mpPass" type="password"></ion-input>
</ion-item>
</ion-list>
<ion-list>
<ion-item>
<ion-label>Confirm</ion-label>
</ion-item>
<ion-item>
<ion-input v-model="mpConfirm" type="password"></ion-input>
</ion-item>
</ion-list>
</div>
<ion-item>
<ion-button
@click="
modalGetPassword?.resolve
? (() => {
modalGetPassword.resolve();
modalGetPassword = null;
})()
: confirmModal()
"
>Confirm</ion-button
>
</ion-item>
</ion-content>
</ion-modal>
<ion-alert
:is-open="alertOpen"
:header="alertHeader"
:message="alertMsg"
:buttons="['OK']"
@didDismiss="alertOpen = false"
></ion-alert>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, Ref } from "vue";
import {
storageWipe,
getSettings,
setSettings,
getAccounts,
saveSelectedAccount,
replaceAccounts,
openTab,
} from "@/utils/platform";
import { decrypt, encrypt, getCryptoParams } from "@/utils/webCrypto";
import { Account } from "@/extension/types";
import { exportFile } from "@/utils/misc";
import type { Settings } from "@/extension/types";
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonLoading,
onIonViewWillEnter,
IonList,
IonToggle,
IonModal,
IonInput,
IonAccordion,
IonAccordionGroup,
IonRadioGroup,
IonRadio,
IonButtons,
IonAlert,
IonToast,
} from "@ionic/vue";
export default defineComponent({
components: {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
IonItem,
IonLabel,
IonButton,
IonLoading,
IonList,
IonToggle,
IonModal,
IonInput,
IonAccordion,
IonAccordionGroup,
IonRadioGroup,
IonRadio,
IonButtons,
IonAlert,
IonToast,
},
setup() {
const loading = ref(true);
const mpModal = ref(false);
const mpPass = ref("");
const mpConfirm = ref("");
const updateKey = ref(0);
const alertOpen = ref(false);
const alertMsg = ref("");
const toastState = ref(false);
const toastMsg = ref("");
const alertHeader = ref("Error");
const importFile = (ref(null) as unknown) as Ref<HTMLInputElement>;
type ModalPromisePassword = null | {
resolve: (p?: unknown) => void;
reject: (p?: unknown) => void;
};
const modalGetPassword = ref(null) as Ref<ModalPromisePassword>;
const noAccounts = ref(true);
const defaultAccordionOpen = ref("0");
const radioTheme = ref("system") as Ref<"system" | "light" | "dark">;
const wipeStorage = async () => {
loading.value = true;
await storageWipe();
loading.value = false;
};
const settings = reactive({
s: (null as unknown) as Settings,
}) as { s: Settings };
const saveSettings = async () => {
loading.value = true;
await setSettings(settings.s);
loading.value = false;
};
const setEncryptToggle = (state: boolean) => {
settings.s.enableStorageEnctyption = state;
updateKey.value++;
defaultAccordionOpen.value = "1";
};
const changeAutoLock = async () => {
settings.s.lockOutEnabled = !settings.s.lockOutEnabled;
updateKey.value++;
await saveSettings();
defaultAccordionOpen.value = "1";
};
const changePermaLock = async () => {
settings.s.encryptAfterEveryTx = !settings.s.encryptAfterEveryTx;
updateKey.value++;
await saveSettings();
defaultAccordionOpen.value = "1";
};
const changeTheme = async (theme: "system" | "light" | "dark") => {
document.body.classList.remove(radioTheme.value);
document.body.classList.add(theme);
radioTheme.value = theme;
settings.s.theme = theme;
await saveSettings();
defaultAccordionOpen.value = "2";
};
const changeEncryption = async () => {
loading.value = true;
mpModal.value = true;
loading.value = false;
};
const confirmModal = async () => {
loading.value = true;
if (mpPass.value.length < 3) {
loading.value = false;
alertHeader.value = "Error";
alertMsg.value = "Password is too short. More than 3 characters are required.";
alertOpen.value = true;
setEncryptToggle(settings.s.enableStorageEnctyption);
return;
}
if (!settings.s.enableStorageEnctyption) {
if (mpPass.value !== mpConfirm.value) {
loading.value = false;
alertHeader.value = "Error";
alertMsg.value = "Password and confirm password do not match";
alertOpen.value = true;
setEncryptToggle(settings.s.enableStorageEnctyption);
return;
}
let accounts = await getAccounts();
const cryptoParams = await getCryptoParams(mpPass.value);
const accProm = accounts.map(async (a) => {
a.encPk = await encrypt(a.pk, cryptoParams);
a.pk = "";
return a;
});
accounts = await Promise.all(accProm);
await replaceAccounts(accounts);
await saveSelectedAccount(accounts[0]);
setEncryptToggle(true);
await setSettings(settings.s);
mpPass.value = "";
mpConfirm.value = "";
mpModal.value = false;
} else {
try {
let accounts = await getAccounts();
const cryptoParams = await getCryptoParams(mpPass.value);
const accProm = accounts.map(async (a) => {
if (a.encPk) {
a.pk = await decrypt(a.encPk, cryptoParams);
}
return a;
});
accProm.forEach((a) => a.catch((e) => console.log(e)));
accounts = await Promise.all(accProm);
await replaceAccounts(accounts);
await saveSelectedAccount(accounts[0]);
setEncryptToggle(false);
settings.s.lockOutEnabled = false;
settings.s.encryptAfterEveryTx = false;
await setSettings(settings.s);
mpPass.value = "";
mpConfirm.value = "";
mpModal.value = false;
} catch (error) {
console.log(error);
loading.value = false;
alertHeader.value = "Error";
alertMsg.value = "Decryption failed, password is not correct.";
alertOpen.value = true;
setEncryptToggle(settings.s.enableStorageEnctyption);
return;
}
}
loading.value = false;
};
const validateFile = () => {
return new Promise((resolve) => {
try {
if (!importFile.value?.value?.length) {
return resolve({
error: "Import json file is missing",
});
}
const reader = new FileReader();
reader.onload = (event) => {
const json = JSON.parse(event?.target?.result as string);
if (!json.length) {
return resolve({
error:
'JSON format is wrong. Corrrect JSON format is: [{ "name": "Account Name", "pk": "Private Key", "address": "0x..." },{...}]',
});
}
const test = json.some(
(e: any) =>
!("pk" in e) ||
!("name" in e) ||
!("address" in e) ||
!(e.pk.length === 66 || e.pk.length === 64)
);
if (test) {
return resolve({
error:
'JSON format is wrong. Corrrect JSON format is: [{ "name": "Account Name", "pk": "Private Key", "address": "0x..." },{...}], Also PK must be valid (66 || 64 length) !',
});
}
return resolve({ error: false, json });
};
reader.readAsText(importFile.value?.files?.[0] as File);
} catch {
return resolve({
error: "Parsing JSON file",
});
}
});
};
const getPassword = () => {
return new Promise((resolve, reject) => {
modalGetPassword.value = { resolve, reject };
mpModal.value = true;
});
};
const promptForPassword = async (accounts: Account[]) => {
let isCorectPass = false;
do {
try {
await getPassword();
modalGetPassword.value = null;
} catch {
alertHeader.value = "Error";
alertMsg.value = "Password is required!";
alertOpen.value = true;
mpModal.value = false;
return false;
}
try {
const cryptoParams = await getCryptoParams(mpPass.value);
if (accounts?.[0]?.encPk) {
await decrypt(accounts[0].encPk, cryptoParams);
}
isCorectPass = true;
} catch {
isCorectPass = false;
alertHeader.value = "Error";
alertMsg.value = "Password is wrong!";
alertOpen.value = true;
}
} while (!isCorectPass);
return true;
};
const importAcc = async () => {
const validation = (await validateFile()) as { error: any };
if (validation.error) {
alertMsg.value = validation.error;
alertOpen.value = true;
return;
}
const accounts = await getAccounts();
const newAccounts = ((validation as unknown) as { json: Account[] }).json;
if (settings.s.enableStorageEnctyption) {
const hasPass = await promptForPassword(accounts);
if (hasPass) {
const cryptoParams = await getCryptoParams(mpPass.value);
const accProm = newAccounts.map(async (a) => {
if (a.pk.length === 64) {
a.pk = `0x${a.pk}`;
}
a.encPk = await encrypt(a.pk, cryptoParams);
return a;
});
const encNewAccounts = await Promise.all(accProm);
await replaceAccounts([...accounts, ...encNewAccounts]);
alertHeader.value = "Success";
alertMsg.value = "Successfully imported new accounts.";
alertOpen.value = true;
noAccounts.value = false;
}
return false;
} else {
await replaceAccounts([
...accounts,
...newAccounts.map((a) => {
a.encPk = "";
return a;
}),
]);
alertHeader.value = "Success";
alertMsg.value = "Successfully imported new accounts.";
alertOpen.value = true;
noAccounts.value = false;
}
};
const exportAcc = async () => {
const accounts = await getAccounts();
if (!accounts.length) {
alertMsg.value = "You need at least one account to export.";
alertOpen.value = true;
}
if (settings.s.enableStorageEnctyption) {
const hasPass = await promptForPassword(accounts);
if (hasPass) {
const cryptoParams = await getCryptoParams(mpPass.value);
const accProm = accounts.map(async (a) => {
a.pk = await decrypt(a.encPk, cryptoParams);
return a;
});
const encNewAccounts = await Promise.all(accProm);
exportFile("wallet_export.json", JSON.stringify(encNewAccounts, null, 2));
}
return false;
} else {
exportFile("wallet_export.json", JSON.stringify(accounts, null, 2));
}
};
onIonViewWillEnter(async () => {
await Promise.all([
getSettings().then((storeSettings) => {
settings.s = storeSettings;
radioTheme.value = settings.s.theme;
}),
getAccounts().then((accounts) => {
if (accounts.length) {
noAccounts.value = false;
}
}),
]);
loading.value = false;
});
const setTime = async () => {
loading.value = true;
if (settings.s.lockOutPeriod < 2 || settings.s.lockOutPeriod > 120) {
loading.value = false;
alertMsg.value = "Auto-lock period must be between 2 and 120";
alertOpen.value = true;
return;
}
settings.s.lockOutPeriod = Math.trunc(settings.s.lockOutPeriod);
await saveSettings();
loading.value = false;
toastMsg.value = "Auto-lock period was set";
toastState.value = true;
};
const modalDismiss = () => {
setEncryptToggle(settings.s.enableStorageEnctyption);
};
return {
wipeStorage,
loading,
mpModal,
settings,
saveSettings,
changeEncryption,
mpPass,
mpConfirm,
confirmModal,
updateKey,
alertOpen,
alertMsg,
modalDismiss,
setTime,
toastState,
toastMsg,
importAcc,
exportAcc,
importFile,
modalGetPassword,
noAccounts,
alertHeader,
changeAutoLock,
defaultAccordionOpen,
changeTheme,
openTab,
radioTheme,
changePermaLock,
};
},
});
</script>