Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add backup page #594

Merged
merged 31 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d2e39c1
feat: add backup page
gamalielhere Jan 8, 2025
1260f74
Merge branch 'develop' into feat/backup-detected
kvhnuke Feb 4, 2025
e81cd7b
devop: backups are functioning
kvhnuke Feb 5, 2025
40d095c
fix: minor ui cleanup
gamalielhere Feb 5, 2025
274981e
devop: cleanup
kvhnuke Feb 5, 2025
00e4e72
devop: backup settings
kvhnuke Feb 5, 2025
e1c02d1
devop: backup functionality complete
kvhnuke Feb 5, 2025
0a327c1
devop: update pakages
kvhnuke Feb 10, 2025
3dbaa9c
devop: switch to new api
kvhnuke Feb 10, 2025
75ed672
feat: clean up table, add loading state, add no backup state
gamalielhere Feb 12, 2025
75a393f
devop: new uuid for each restore
kvhnuke Feb 12, 2025
7888dbc
feat: updates
gamalielhere Feb 12, 2025
3cf90f1
Merge branch 'feat/backup-detected' of github.com:enkryptcom/enKrypt …
gamalielhere Feb 12, 2025
73f87c2
devop: generate names for uuids
kvhnuke Feb 13, 2025
d11eca8
devop: clean up and loading state
gamalielhere Feb 13, 2025
2171ae8
fix: conflicts
gamalielhere Feb 13, 2025
10cd0ca
fix: change titles
gamalielhere Feb 13, 2025
c315046
fix: remove underscore
gamalielhere Feb 13, 2025
5abdb83
Merge branch 'devop/upgrade-packages' of github.com:enkryptcom/enKryp…
gamalielhere Feb 13, 2025
829098a
feat: cleanup, enable identicon
gamalielhere Feb 13, 2025
7748adb
feat: add delete confirmation page for backups
gamalielhere Feb 14, 2025
eff1a45
chore: margins
gamalielhere Feb 14, 2025
3a8ab1d
devop: switch url
kvhnuke Feb 14, 2025
c47a88f
Merge branch 'feat/backup-detected' of github.com:enkryptcom/enKrypt …
kvhnuke Feb 14, 2025
ebe36dc
feat: loading state for backup page
gamalielhere Feb 18, 2025
27086e8
Merge branch 'feat/backup-detected' of github.com:enkryptcom/enKrypt …
gamalielhere Feb 18, 2025
ff20eec
chore: copy changes and margin
gamalielhere Feb 18, 2025
c811fbd
fix: spacing
gamalielhere Feb 18, 2025
fa715f0
fix: backup on new wallets
kvhnuke Feb 18, 2025
213808c
fix: close delete modal after deleting
gamalielhere Feb 19, 2025
3f81643
Merge branch 'feat/backup-detected' of github.com:enkryptcom/enKrypt …
gamalielhere Feb 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/extension/src/libs/backup-state/configs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const BACKUP_URL = 'https://rabbit.ethvm.dev/';
const HEADERS = {
Accept: 'application/json',
'Content-Type': 'application/json',
};
export { BACKUP_URL, HEADERS };
365 changes: 365 additions & 0 deletions packages/extension/src/libs/backup-state/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
import BrowserStorage from '../common/browser-storage';
import { InternalStorageNamespace } from '@/types/provider';
import {
BackupData,
BackupResponseType,
BackupType,
IState,
StorageKeys,
} from './types';
import PublicKeyRing from '../keyring/public-keyring';
import sendUsingInternalMessengers from '../messenger/internal-messenger';
import { InternalMethods } from '@/types/messenger';
import EthNetwork from '@/providers/ethereum/networks/eth';
import {
bufferToHex,
hexToBuffer,
NACL_VERSION,
naclEncrypt,
utf8ToHex,
} from '@enkryptcom/utils';
import { hashPersonalMessage } from '@ethereumjs/util';
import { v4 as uuidv4 } from 'uuid';
import { BACKUP_URL, HEADERS } from './configs';
import { EnkryptAccount, SignerType, WalletType } from '@enkryptcom/types';
import KeyRingBase from '../keyring/keyring';

class BackupState {
private storage: BrowserStorage;

constructor() {
this.storage = new BrowserStorage(InternalStorageNamespace.backupState);
}

async getMainWallet(): Promise<EnkryptAccount> {
const pkr = new PublicKeyRing();
const allAccounts = await pkr.getAccounts();
const mainWallet = allAccounts.find(
acc =>
acc.walletType === WalletType.mnemonic &&
acc.pathIndex === 0 &&
acc.signerType === EthNetwork.signer[0] &&
acc.basePath === EthNetwork.basePath,
);
if (!mainWallet) {
throw new Error('No main wallet found');
}
return mainWallet;
}

getBackupSigHash(pubkey: string): string {
const now = new Date();
const messageToSign = `${pubkey}-GET-BACKUPS-${(now.getUTCMonth() + 1).toString().padStart(2, '0')}-${now.getUTCDate().toString().padStart(2, '0')}-${now.getUTCFullYear()}`;
return bufferToHex(
hashPersonalMessage(hexToBuffer(utf8ToHex(messageToSign))),
);
}
async getBackups(options?: {
signature: string;
pubkey: string;
}): Promise<BackupType[]> {
let signature: string = '';
let pubkey: string = '';
if (options) {
signature = options.signature;
pubkey = options.pubkey;
} else {
const mainWallet = await this.getMainWallet();
pubkey = mainWallet.publicKey;
const msgHash = this.getBackupSigHash(mainWallet.publicKey);
console.log('get backups signature', msgHash, mainWallet);
signature = await sendUsingInternalMessengers({
method: InternalMethods.sign,
params: [msgHash, mainWallet],
}).then(res => {
if (res.error) {
console.error(res);
return null;
} else {
return JSON.parse(res.result as string);
}
});
}
if (!signature) {
console.error('No signature found');
return [];
}
console.log('get backups signature', signature);

const rawResponse = await fetch(
`${BACKUP_URL}backups/${pubkey}?signature=${signature}`,
{
method: 'GET',
headers: HEADERS,
},
);
const content: BackupResponseType = await rawResponse.json();
return content.backups;
}

async deleteBackup(userId: string): Promise<boolean> {
const mainWallet = await this.getMainWallet();
const now = new Date();
const messageToSign = `${userId}-DELETE-BACKUP-${(now.getUTCMonth() + 1).toString().padStart(2, '0')}-${now.getUTCDate().toString().padStart(2, '0')}-${now.getUTCFullYear()}`;
const msgHash = bufferToHex(
hashPersonalMessage(hexToBuffer(utf8ToHex(messageToSign))),
);
const signature = await sendUsingInternalMessengers({
method: InternalMethods.sign,
params: [msgHash, mainWallet],
}).then(res => {
if (res.error) {
console.error(res);
return null;
} else {
return JSON.parse(res.result as string);
}
});
if (!signature) {
console.error('No signature found');
return false;
}
console.log('delete signature', signature);
return fetch(
`${BACKUP_URL}backups/${mainWallet.publicKey}/users/${userId}?signature=${signature}`,
{
method: 'DELETE',
headers: HEADERS,
},
)
.then(res => res.json())
.then(content => {
if (content.message === 'Ok') {
return true;
}
console.error(content);
return false;
});
}

async restoreBackup(
backup: BackupType,
keyringPassword: string,
): Promise<void> {
const mainWallet = await this.getMainWallet();
await sendUsingInternalMessengers({
method: InternalMethods.unlock,
params: [keyringPassword, false],
});
await sendUsingInternalMessengers({
method: InternalMethods.ethereumDecrypt,
params: [backup.payload, mainWallet],
}).then(async res => {
if (res.error) {
console.error(res);
return null;
} else {
const kr = new KeyRingBase();
await kr.unlock(keyringPassword);
const existingAccounts = await kr.getKeysArray();
const decryptedBackup: BackupData = JSON.parse(
JSON.parse(res.result as string),
);
console.log(decryptedBackup);
const highestPathIndex: Record<string, number> = {};
decryptedBackup.accounts.forEach(acc => {
const id = `${acc.basePath}###${acc.signerType}`;
const idx = acc.pathIndex;
if (!highestPathIndex[id] || highestPathIndex[id] < idx) {
highestPathIndex[id] = idx;
}
});

console.log(highestPathIndex);
const getAccountByIndex = (
accounts: Omit<EnkryptAccount, 'address' | 'publicKey'>[],
basePath: string,
signerType: SignerType,
idx: number,
): EnkryptAccount | null => {
for (const acc of accounts) {
if (
acc.basePath === basePath &&
acc.pathIndex === idx &&
acc.signerType === signerType
) {
return acc as EnkryptAccount;
}
}
return null;
};

for (const key of Object.keys(highestPathIndex)) {
const [basePath, signerType] = key.split('###');
for (let i = 0; i <= highestPathIndex[key]; i++) {
const newAccount = getAccountByIndex(
decryptedBackup.accounts,
basePath,
signerType as SignerType,
i,
);
const existingAccount = getAccountByIndex(
existingAccounts,
basePath,
signerType as SignerType,
i,
);
if (existingAccount && newAccount) {
console.log('Account already exists, just renaming');
await kr.renameAccount(existingAccount.address, newAccount.name);
continue;
} else if (newAccount) {
console.log('creating new account', newAccount);
await kr.saveNewAccount({
basePath: newAccount.basePath,
name: newAccount.name,
signerType: newAccount.signerType,
walletType: newAccount.walletType,
});
} else if (!newAccount) {
console.log('edge case shouldnt happen', newAccount);
await kr.saveNewAccount({
basePath: basePath,
name: `New Account from backup ${i}`,
signerType: signerType as SignerType,
walletType: WalletType.mnemonic,
});
}
}
}
await this.setUserId(decryptedBackup.uuid);
}
});
}

async backup(firstTime: boolean): Promise<boolean> {
const state = await this.getState();
if (firstTime && state.lastBackupTime !== 0) {
return true;
}
if (!state.enabled) {
return true;
}
const pkr = new PublicKeyRing();
const allAccounts = await pkr.getAccounts();
const mainWallet = await this.getMainWallet();
const backupData: BackupData = {
accounts: allAccounts
.filter(
acc => !acc.isTestWallet && acc.walletType !== WalletType.privkey,
)
.map(acc => {
return {
basePath: acc.basePath,
pathIndex: acc.pathIndex,
name: acc.name,
signerType: acc.signerType,
walletType: acc.walletType,
isHardware: acc.isHardware,
HWOptions: acc.HWOptions,
};
}),
uuid: state.userId,
};
const encryptPubKey = await sendUsingInternalMessengers({
method: InternalMethods.getEthereumEncryptionPublicKey,
params: [mainWallet],
}).then(res => {
if (res.error) {
console.error(res);
return null;
} else {
return JSON.parse(res.result as string);
}
});
if (!encryptPubKey) {
console.error('No encrypt public key found');
return false;
}
const encryptedStr = naclEncrypt({
publicKey: encryptPubKey,
data: JSON.stringify(backupData),
version: NACL_VERSION,
});
const msgHash = bufferToHex(hashPersonalMessage(hexToBuffer(encryptedStr)));
return sendUsingInternalMessengers({
method: InternalMethods.sign,
params: [msgHash, mainWallet],
}).then(async res => {
if (res.error) {
console.error(res);
return false;
} else {
const rawResponse = await fetch(
`${BACKUP_URL}backups/${mainWallet.publicKey}/users/${state.userId}?signature=${JSON.parse(res.result as string)}`,
{
method: 'POST',
headers: HEADERS,
body: JSON.stringify({
payload: encryptedStr,
}),
},
);
const content = await rawResponse.json();
if (content.message === 'Ok') {
await this.setState({
lastBackupTime: new Date().getTime(),
userId: state.userId,
enabled: state.enabled,
});
return true;
}
console.error(content);
return false;
}
});
}

async setState(state: IState): Promise<void> {
return this.storage.set(StorageKeys.backupInfo, state);
}

async getState(): Promise<IState> {
const state = await this.storage.get(StorageKeys.backupInfo);
if (!state) {
const newState: IState = {
lastBackupTime: 0,
userId: uuidv4(),
enabled: true,
};
await this.setState(newState);
return newState;
}
return state;
}

async getLastUpdatedTime(): Promise<Date> {
const state: IState = await this.getState();
return new Date(state.lastBackupTime);
}

async getUserId(): Promise<string> {
const state: IState = await this.getState();
return state.userId;
}

async setUserId(userId: string): Promise<void> {
const state: IState = await this.getState();
await this.setState({ ...state, userId });
}

async disableBackups(): Promise<void> {
const state: IState = await this.getState();
await this.setState({ ...state, enabled: false });
}
async enableBackups(): Promise<void> {
const state: IState = await this.getState();
await this.setState({ ...state, enabled: true });
}
async isBackupEnabled(): Promise<boolean> {
const state: IState = await this.getState();
return state.enabled;
}
}

export default BackupState;
25 changes: 25 additions & 0 deletions packages/extension/src/libs/backup-state/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EnkryptAccount } from '@enkryptcom/types';

export enum StorageKeys {
backupInfo = 'backup-info',
}

export interface IState {
lastBackupTime: number;
userId: string;
enabled: boolean;
}

export interface BackupType {
userId: string;
payload: string;
updatedAt: string;
}
export interface BackupResponseType {
backups: BackupType[];
}

export interface BackupData {
accounts: Omit<EnkryptAccount, 'address' | 'publicKey'>[];
uuid: string;
}
Loading
Loading