diff --git a/src/App.tsx b/src/App.tsx index 0f74145..9a058c1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -115,6 +115,7 @@ import { Tutorials } from "./components/Tutorials/Tutorials"; import { useHandleTutorials } from "./components/Tutorials/useHandleTutorials"; import { CoreSyncStatus } from "./components/CoreSyncStatus"; import BoundedNumericTextField from "./common/BoundedNumericTextField"; +import { Wallets } from "./Wallets"; type extStates = | "not-authenticated" @@ -919,7 +920,12 @@ function App() { ) return; - setExtstate("authenticated"); + if(response?.hasKeyPair){ + setExtstate("authenticated"); + + } else { + setExtstate("wallet-dropped"); + } } }); } catch (error) { @@ -1069,7 +1075,7 @@ function App() { try { if(hasSettingsChanged){ await showUnsavedChanges({message: 'Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.'}) - } if(extState === 'authenticated') { + } else if(extState === 'authenticated') { await showUnsavedChanges({ message: "Are you sure you would like to logout?", @@ -1831,7 +1837,7 @@ function App() { value={paymentPassword} onChange={(e) => setPaymentPassword(e.target.value)} autoComplete="off" - ref={passwordRef} + /> @@ -2321,6 +2327,34 @@ function App() { Create account + )} + {extState === "wallets" && ( + <> + + + { + setRawWallet(null); + setExtstate("not-authenticated"); + logoutFunc(); + }} + src={Return} + /> + + + + )} {rawWallet && extState === "wallet-dropped" && ( <> @@ -2340,7 +2374,8 @@ function App() { }} onClick={() => { setRawWallet(null); - setExtstate("not-authenticated"); + setExtstate("wallets"); + logoutFunc(); }} src={Return} /> @@ -2361,9 +2396,11 @@ function App() { sx={{ display: "flex", flexDirection: "column", - alignItems: "flex-start", + alignItems: "center", }} > + {rawWallet?.name ? rawWallet?.name : rawWallet?.address0} + diff --git a/src/ExtStates/NotAuthenticated.tsx b/src/ExtStates/NotAuthenticated.tsx index 44e8db7..e64adba 100644 --- a/src/ExtStates/NotAuthenticated.tsx +++ b/src/ExtStates/NotAuthenticated.tsx @@ -251,16 +251,11 @@ export const NotAuthenticated = ({ display: "flex", gap: "10px", alignItems: "center", - marginLeft: "28px", }} > - - - Authenticate + setExtstate('wallets')}> + Wallets - - - @@ -269,7 +264,6 @@ export const NotAuthenticated = ({ display: "flex", gap: "10px", alignItems: "center", - marginLeft: "28px", }} > - + diff --git a/src/Wallets.tsx b/src/Wallets.tsx new file mode 100644 index 0000000..63c6b60 --- /dev/null +++ b/src/Wallets.tsx @@ -0,0 +1,482 @@ +import React, { useEffect, useRef, useState } from "react"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import Divider from "@mui/material/Divider"; +import ListItemText from "@mui/material/ListItemText"; +import ListItemAvatar from "@mui/material/ListItemAvatar"; +import Avatar from "@mui/material/Avatar"; +import Typography from "@mui/material/Typography"; +import { Box, Button, ButtonBase, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Input } from "@mui/material"; +import { CustomButton } from "./App-styles"; +import { useDropzone } from "react-dropzone"; +import EditIcon from "@mui/icons-material/Edit"; +import { Label } from "./components/Group/AddGroup"; +import { Spacer } from "./common/Spacer"; +import { getWallets, storeWallets } from "./background"; +import { useModal } from "./common/useModal"; +import PhraseWallet from "./utils/generateWallet/phrase-wallet"; +import { decryptStoredWalletFromSeedPhrase } from "./utils/decryptWallet"; +import { crypto, walletVersion } from "./constants/decryptWallet"; +import { LoadingButton } from "@mui/lab"; +import { PasswordField } from "./components"; + +const parsefilenameQortal = (filename)=> { + return filename.startsWith("qortal_backup_") ? filename.slice(14) : filename; + } + +export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => { + const [wallets, setWallets] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [seedValue, setSeedValue] = useState(""); + const [seedName, setSeedName] = useState(""); + const [seedError, setSeedError] = useState(""); + + const [password, setPassword] = useState(""); + const [isOpenSeedModal, setIsOpenSeedModal] = useState(false); + const [isLoadingEncryptSeed, setIsLoadingEncryptSeed] = useState(false); + + const { isShow, onCancel, onOk, show, } = useModal(); + + const { getRootProps, getInputProps } = useDropzone({ + accept: { + "application/json": [".json"], // Only accept JSON files + }, + onDrop: async (acceptedFiles) => { + const files: any = acceptedFiles; + let importedWallets: any = []; + + for (const file of files) { + try { + const fileContents = await new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onabort = () => reject("File reading was aborted"); + reader.onerror = () => reject("File reading has failed"); + reader.onload = () => { + // Resolve the promise with the reader result when reading completes + resolve(reader.result); + }; + + // Read the file as text + reader.readAsText(file); + }); + if (typeof fileContents !== "string") continue; + const parsedData = JSON.parse(fileContents) + importedWallets.push({...parsedData, filename: file?.name}); + } catch (error) { + console.error(error); + } + } + + let error: any = null; + let uniqueInitialMap = new Map(); + + // Only add a message if it doesn't already exist in the Map + importedWallets.forEach((wallet) => { + if (!wallet?.address0) return; + if (!uniqueInitialMap.has(wallet?.address0)) { + uniqueInitialMap.set(wallet?.address0, wallet); + } + }); + const data = Array.from(uniqueInitialMap.values()); + if (data && data?.length > 0) { + const uniqueNewWallets = data.filter( + (newWallet) => + !wallets.some( + (existingWallet) => + existingWallet?.address0 === newWallet?.address0 + ) + ); + setWallets([...wallets, ...uniqueNewWallets]); + } + }, + }); + + const updateWalletItem = (idx, wallet) => { + setWallets((prev) => { + let copyPrev = [...prev]; + if (wallet === null) { + copyPrev.splice(idx, 1); // Use splice to remove the item + return copyPrev; + } else { + copyPrev[idx] = wallet; // Update the wallet at the specified index + return copyPrev; + } + }); + }; + + const handleSetSeedValue = async ()=> { + try { + setIsOpenSeedModal(true) + const {seedValue, seedName, password} = await show({ + message: "", + publishFee: "", + }); + setIsLoadingEncryptSeed(true) + const res = await decryptStoredWalletFromSeedPhrase(seedValue) + const wallet2 = new PhraseWallet(res, walletVersion); + const wallet = await wallet2.generateSaveWalletData( + password, + crypto.kdfThreads, + () => {} + ); + if(wallet?.address0){ + setWallets([...wallets, { + ...wallet, + name: seedName + }]); + setIsOpenSeedModal(false) + setSeedValue('') + setSeedName('') + setPassword('') + setSeedError('') + } else { + setSeedError('Could not create wallet.') + } + + } catch (error) { + setSeedError(error?.message || 'Could not create wallet.') + } finally { + setIsLoadingEncryptSeed(false) + } + } + + const selectedWalletFunc = (wallet) => { + setRawWallet(wallet); + setExtState("wallet-dropped"); + }; + + useEffect(()=> { + setIsLoading(true) + getWallets().then((res)=> { + + if(res && Array.isArray(res)){ + setWallets(res) + } + setIsLoading(false) + }).catch((error)=> { + console.error(error) + setIsLoading(false) + }) + }, []) + + useEffect(()=> { + if(!isLoading && wallets && Array.isArray(wallets)){ + storeWallets(wallets) + } + }, [wallets, isLoading]) + + if(isLoading) return null + + return ( +
+ {(wallets?.length === 0 || + !wallets) ? ( + <> + No wallets saved + + + ): ( + <> + Your saved wallets + + + )} + + {rawWallet && ( + + Selected Wallet: + {rawWallet?.name && {rawWallet.name}} + {rawWallet?.address0 && ( + {rawWallet?.address0} + )} + + )} + {wallets?.length > 0 && ( + + {wallets?.map((wallet, idx) => { + return ( + <> + + + + ); + })} + + )} + + + + + Add seed-phrase + + + + Add wallets + + + + { + if (e.key === 'Enter' && seedValue && seedName && password) { + onOk({seedValue, seedName, password}); + } + }} + > + + Type or paste in your seed-phrase + + + + + setSeedName(e.target.value)} + /> + + + setSeedValue(e.target.value)} + /> + + + + setPassword(e.target.value)} + autoComplete="off" + /> + + + + + + + { + if(!seedValue || !seedName || !password) return + onOk({seedValue, seedName, password}); + }} + autoFocus + > + Add + + {seedError} + + + +
+ + ); +}; + +const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => { + const [name, setName] = useState(""); + const [note, setNote] = useState(""); + const [isEdit, setIsEdit] = useState(false); + + useEffect(() => { + if (wallet?.name) { + setName(wallet.name); + } + if (wallet?.note) { + setNote(wallet.note); + } + }, [wallet]); + return ( + <> + { + setSelectedWallet(wallet); + }} + sx={{ + width: '100%' + }} + > + { + e.stopPropagation(); + setIsEdit(true); + }} + edge="end" + aria-label="edit" + > + + + } + alignItems="flex-start" + > + + + + + + {wallet?.address0} + + {wallet?.note} + + } + /> + + + {isEdit && ( + + + setName(e.target.value)} + sx={{ + width: "100%", + }} + /> + + + setNote(e.target.value)} + inputProps={{ + maxLength: 100, + }} + sx={{ + width: "100%", + }} + /> + + + + + + + + )} + + + + + ); +}; diff --git a/src/assets/Icons/AppsIcon.tsx b/src/assets/Icons/AppsIcon.tsx new file mode 100644 index 0000000..753fe17 --- /dev/null +++ b/src/assets/Icons/AppsIcon.tsx @@ -0,0 +1,50 @@ +import React from "react"; + +export const AppsIcon = ({ color, height = 31, width = 31 }) => { + return ( + + + + + + + + + + + + ); +}; diff --git a/src/background.ts b/src/background.ts index 2892114..aba146b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -33,6 +33,47 @@ import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMu import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes"; import TradeBotRespondRequest from './transactions/TradeBotRespondRequest'; + +let inMemoryKey: CryptoKey | null = null; +let inMemoryIV: Uint8Array | null = null; + + +async function initializeKeyAndIV() { + if (!inMemoryKey) { + inMemoryKey = await generateKey(); // Generates the key in memory + } +} + +async function generateKey(): Promise { + return await crypto.subtle.generateKey( + { + name: "AES-GCM", + length: 256 + }, + true, + ["encrypt", "decrypt"] + ); +} + +async function encryptData(data: string, key: CryptoKey): Promise<{ iv: Uint8Array; encryptedData: ArrayBuffer }> { + const encoder = new TextEncoder(); + const encodedData = encoder.encode(data); + + // Generate a random IV each time you encrypt + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const encryptedData = await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv + }, + key, + encodedData + ); + + return { iv, encryptedData }; +} + export function cleanUrl(url) { return url?.replace(/^(https?:\/\/)?(www\.)?/, ''); } @@ -997,11 +1038,41 @@ async function getAddressInfo(address) { } return data; } +async function decryptData(encryptedData: ArrayBuffer, key: CryptoKey, iv: Uint8Array): Promise { + const decryptedData = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv + }, + key, + encryptedData + ); + + const decoder = new TextDecoder(); + return decoder.decode(decryptedData); +} + +function base64ToJson(base64) { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return JSON.parse(new TextDecoder().decode(bytes)); +} export async function getKeyPair() { const res = await chrome.storage.local.get(["keyPair"]); if (res?.keyPair) { - return res.keyPair; + const combinedData = atob(res?.keyPair) + .split("") + .map((c) => c.charCodeAt(0)); + + const iv = new Uint8Array(combinedData.slice(0, 12)); // First 12 bytes are the IV + const encryptedData = new Uint8Array(combinedData.slice(12)).buffer; + + const decryptedBase64Data = await decryptData(encryptedData, inMemoryKey, iv); + return decryptedBase64Data } else { throw new Error("Wallet not authenticated"); } @@ -1016,6 +1087,25 @@ export async function getSaveWallet() { } } +export async function getWallets() { + const res = await chrome.storage.local.get(["wallets"]); + if (res['wallets']) { + return res['wallets']; + } else { + throw new Error("No wallet saved"); + } +} + +export async function storeWallets(wallets) { + chrome.storage.local.set({ wallets: wallets }, () => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(true); + } + }); +} + async function clearAllNotifications() { const notifications = await chrome.notifications.getAll(); for (const notificationId of Object.keys(notifications)) { @@ -1483,8 +1573,14 @@ async function decryptWallet({ password, wallet, walletVersion }) { rvnPrivateKey: wallet2._addresses[0].rvnWallet.derivedMasterPrivateKey }; const dataString = JSON.stringify(toSave); + await initializeKeyAndIV(); + const { iv, encryptedData } = await encryptData(dataString, inMemoryKey); + + // Combine IV and encrypted data into a single Uint8Array + const combinedData = new Uint8Array([...iv, ...new Uint8Array(encryptedData)]); + const encryptedBase64Data = btoa(String.fromCharCode(...combinedData)); await new Promise((resolve, reject) => { - chrome.storage.local.set({ keyPair: dataString }, () => { + chrome.storage.local.set({ keyPair: encryptedBase64Data }, () => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); } else { @@ -3209,14 +3305,22 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { if (chrome.runtime.lastError) { sendResponse({ error: chrome.runtime.lastError.message }); } else if (result.walletInfo) { - sendResponse({ walletInfo: result.walletInfo }); + sendResponse({ walletInfo: result.walletInfo, hasKeyPair: true }); } else { sendResponse({ error: "No wallet info found" }); } }); }) .catch((error) => { - sendResponse({ error: error.message }); + chrome.storage.local.get(["walletInfo"], (result) => { + if (chrome.runtime.lastError) { + sendResponse({ error: chrome.runtime.lastError.message }); + } else if (result.walletInfo) { + sendResponse({ walletInfo: result.walletInfo, hasKeyPair: false }); + } else { + sendResponse({ error: "Wallet not authenticated" }); + } + }); }); break; diff --git a/src/backgroundFunctions/encryption.ts b/src/backgroundFunctions/encryption.ts index 5c1feb0..6af27c4 100644 --- a/src/backgroundFunctions/encryption.ts +++ b/src/backgroundFunctions/encryption.ts @@ -1,4 +1,4 @@ -import { getBaseApi } from "../background"; +import { getBaseApi, getKeyPair } from "../background"; import { createSymmetricKeyAndNonce, decryptGroupData, encryptDataGroup, objectToBase64 } from "../qdn/encryption/group-encryption"; import { publishData } from "../qdn/publish/pubish"; @@ -55,14 +55,14 @@ export async function getNameInfo() { return ""; } } -async function getKeyPair() { - const res = await chrome.storage.local.get(["keyPair"]); - if (res?.keyPair) { - return res.keyPair; - } else { - throw new Error("Wallet not authenticated"); - } - } +// async function getKeyPair() { +// const res = await chrome.storage.local.get(["keyPair"]); +// if (res?.keyPair) { +// return res.keyPair; +// } else { +// throw new Error("Wallet not authenticated"); +// } +// } const getPublicKeys = async (groupNumber: number) => { const validApi = await getBaseApi() const response = await fetch(`${validApi}/groups/members/${groupNumber}?limit=0`); diff --git a/src/qdn/publish/pubish.ts b/src/qdn/publish/pubish.ts index a57fd1e..f114583 100644 --- a/src/qdn/publish/pubish.ts +++ b/src/qdn/publish/pubish.ts @@ -4,7 +4,7 @@ import { Buffer } from "buffer" import Base58 from "../../deps/Base58" import nacl from "../../deps/nacl-fast" import utils from "../../utils/utils" -import { createEndpoint, getBaseApi } from "../../background"; +import { createEndpoint, getBaseApi, getKeyPair } from "../../background"; export async function reusableGet(endpoint){ const validApi = await getBaseApi(); @@ -33,14 +33,14 @@ export async function reusableGet(endpoint){ return data } -async function getKeyPair() { - const res = await chrome.storage.local.get(["keyPair"]); - if (res?.keyPair) { - return res.keyPair; - } else { - throw new Error("Wallet not authenticated"); - } - } +// async function getKeyPair() { +// const res = await chrome.storage.local.get(["keyPair"]); +// if (res?.keyPair) { +// return res.keyPair; +// } else { +// throw new Error("Wallet not authenticated"); +// } +// } export const publishData = async ({ registeredName, diff --git a/src/utils/decryptWallet.ts b/src/utils/decryptWallet.ts index 21490aa..b091639 100644 --- a/src/utils/decryptWallet.ts +++ b/src/utils/decryptWallet.ts @@ -21,4 +21,13 @@ export const decryptStoredWallet = async (password, wallet) => { } const decryptedBytes = AES_CBC.decrypt(encryptedSeedBytes, encryptionKey, false, iv) return decryptedBytes +} + +export const decryptStoredWalletFromSeedPhrase = async (password) => { + const threads = doInitWorkers(crypto.kdfThreads) + const salt = new Uint8Array(void 0) + + + const seed = await kdf(password, salt, threads) + return seed } \ No newline at end of file