Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions packages/extension/src/libs/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
sendToTab,
newAccount,
lock,
getPrivateKey,
} from './internal';
import { handlePersistentEvents } from './external';
import SettingsState from '../settings-state';
Expand Down Expand Up @@ -168,6 +169,8 @@ class BackgroundHandler {
case InternalMethods.getNewAccount:
case InternalMethods.saveNewAccount:
return newAccount(this.#keyring, message);
case InternalMethods.getPrivateKey:
return getPrivateKey(this.#keyring, message);
default:
return Promise.resolve({
error: getCustomError(
Expand Down
28 changes: 28 additions & 0 deletions packages/extension/src/libs/background/internal/get-private-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getCustomError } from '@/libs/error'
import KeyRingBase from '@/libs/keyring/keyring'
import { InternalOnMessageResponse } from '@/types/messenger'
import { EnkryptAccount, RPCRequestType } from '@enkryptcom/types'

const getPrivateKey = async (
keyring: KeyRingBase,
message: RPCRequestType,
): Promise<InternalOnMessageResponse> => {
if (!message.params || message.params.length < 2)
return {
error: getCustomError('background: invalid params for getting private key'),
}
const account = message.params[0] as EnkryptAccount
const password = message.params[1] as string
try {
const privKey = await keyring.getPrivateKey(account, password)
return {
result: JSON.stringify(privKey),
}
Comment on lines +14 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use SignOptions, return plain string, and preserve ProviderError.

  • KeyRingBase.getPrivateKey expects SignOptions, not EnkryptAccount. Cast/validate accordingly to avoid type drift.
  • Return the private key as a plain string; JSON-wrapping forces callers to JSON.parse.
  • Preserve code/message if the thrown error is already a ProviderError.
-import { EnkryptAccount, RPCRequestType } from '@enkryptcom/types'
+import { RPCRequestType, SignOptions } from '@enkryptcom/types'
@@
-  const account = message.params[0] as EnkryptAccount
+  const options = message.params[0] as SignOptions
   const password = message.params[1] as string
   try {
-    const privKey = await keyring.getPrivateKey(account, password)
+    const privKey = await keyring.getPrivateKey(options, password)
     return {
-      result: JSON.stringify(privKey),
+      result: privKey,
     }
   } catch (e: any) {
-    return {
-      error: getCustomError(e.message),
-    }
+    // If it's already a ProviderError, forward it; else wrap it.
+    if (e && typeof e === 'object' && 'code' in e && 'message' in e) {
+      return { error: e }
+    }
+    return { error: getCustomError(e?.message ?? 'unknown error') }
   }

I can also add runtime shape checks to coerce EnkryptAccount into SignOptions if you prefer keeping the UI payload unchanged.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const account = message.params[0] as EnkryptAccount
const password = message.params[1] as string
try {
const privKey = await keyring.getPrivateKey(account, password)
return {
result: JSON.stringify(privKey),
}
import { RPCRequestType, SignOptions } from '@enkryptcom/types'
// …other imports…
const options = message.params[0] as SignOptions
const password = message.params[1] as string
try {
const privKey = await keyring.getPrivateKey(options, password)
return {
result: privKey,
}
} catch (e: any) {
// If it's already a ProviderError, forward it; else wrap it.
if (e && typeof e === 'object' && 'code' in e && 'message' in e) {
return { error: e }
}
return { error: getCustomError(e?.message ?? 'unknown error') }
}
🤖 Prompt for AI Agents
In packages/extension/src/libs/background/internal/get-private-key.ts around
lines 14 to 20, the handler currently passes an EnkryptAccount and JSON-wraps
the private key while losing ProviderError details; change it to
convert/validate the incoming payload into SignOptions (or coerce EnkryptAccount
-> SignOptions with runtime checks), call keyring.getPrivateKey(signOptions,
password) with the correct type, return the private key as a raw string in
result (not JSON.stringify), and when catching errors rethrow/preserve existing
ProviderError instances (preserve their code/message) instead of wrapping or
discarding them.

} catch (e: any) {
return {
error: getCustomError(e.message),
}
}
}

export default getPrivateKey
2 changes: 2 additions & 0 deletions packages/extension/src/libs/background/internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import changeNetwork from './change-network';
import sendToTab from './send-to-tab';
import newAccount from './new-account';
import lock from './lock';
import getPrivateKey from './get-private-key';
export {
sign,
getEthereumPubKey,
Expand All @@ -15,4 +16,5 @@ export {
sendToTab,
newAccount,
lock,
getPrivateKey,
};
3 changes: 3 additions & 0 deletions packages/extension/src/libs/keyring/keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,8 @@ export class KeyRingBase {
deleteAccount(address: string): Promise<void> {
return this.#keyring.deleteAccount(address);
}
getPrivateKey(options: SignOptions, password: string): Promise<string> {
return this.#keyring.getPrivateKey(options, password);
}
}
export default KeyRingBase;
1 change: 1 addition & 0 deletions packages/extension/src/types/messenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export enum InternalMethods {
getNewAccount = 'enkrypt_getNewAccount',
saveNewAccount = 'enkrypt_saveNewAccount',
changeNetwork = 'enkrypt_changeNetwork',
getPrivateKey = 'enkrypt_getPrivateKey',
}
Comment on lines +38 to 39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Introducing getPrivateKey: confirm strict gating and scope.
Ensure this internal method is only callable from trusted endpoints, requires an unlocked keyring + fresh user confirmation, and never responds to content scripts.

Suggested checks (background handler):

  • Validate sender.origin against allowlist.
  • Require re-auth with password/PIN per invocation.
  • Rate-limit and audit-log export attempts.
  • Return ephemeral data (avoid storing in Redux/state), disable clipboard auto-copy, and scrub from memory/UI ASAP.

You can locate uses with:

Optional typing tweak:

  • Consider typing result as a branded Hex type (e.g., HexString) instead of plain string in InternalOnMessageResponse.

🏁 Script executed:

#!/bin/bash
rg -nP -C3 'InternalMethods\.(getPrivateKey)|["'\'']enkrypt_getPrivateKey["'\'']'

Length of output: 2027


Enforce strict access controls for getPrivateKey

  • In packages/extension/src/libs/background/index.ts, don’t call getPrivateKey unconditionally—validate sender.origin against your allowlist and require a fresh password/PIN unlock of the keyring per request.
  • Rate-limit and audit-log each export; return the key as ephemeral data (no Redux/UI storage), disable any auto-copy, and scrub it from memory/UI immediately.
  • (Optional) Brand the return type as HexString instead of a raw string in InternalOnMessageResponse.
🤖 Prompt for AI Agents
In packages/extension/src/types/messenger.ts around lines 38-39, the
getPrivateKey message is defined but background handlers currently call it
unconditionally; update the background handler in
packages/extension/src/libs/background/index.ts to enforce strict controls:
validate sender.origin against a configured allowlist before proceeding, require
an explicit per-request keyring unlock (password/PIN prompt) and fail if not
provided, apply rate-limiting and write an audit log entry for each export
attempt, return the key as ephemeral data (preferably branded as HexString in
InternalOnMessageResponse) without persisting to Redux/UI or auto-copying, and
immediately scrub the key from memory/UI after use.

export interface SendMessage {
[key: string]: any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,28 @@
>
<delete-icon /><span class="delete">Delete</span>
</a>
<a
v-if="exportable"
class="accounts-item__edit-item"
@click.stop="$emit('action:export')"
>
<view-icon /><span class="export">Export</span>
</a>
</div>
</template>

<script setup lang="ts">
import EditIcon from '@action/icons/actions/edit.vue';
import DeleteIcon from '@action/icons/actions/delete.vue';
import ViewIcon from '@action/icons/actions/view.vue';
defineEmits<{
(e: 'action:rename'): void;
(e: 'action:delete'): void;
(e: 'action:export'): void;
}>();
defineProps({
deletable: Boolean,
exportable: Boolean,
});
</script>

Expand Down Expand Up @@ -77,6 +87,10 @@ defineProps({
&.delete {
color: @error;
}

&.export {
color: @grayPrimary;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
v-if="openEdit"
ref="dropdown"
:deletable="deletable"
:exportable="exportable"
v-bind="$attrs"
/>
</div>
Expand Down Expand Up @@ -78,6 +79,7 @@ defineProps({
active: Boolean,
showEdit: Boolean,
deletable: Boolean,
exportable: Boolean,
});

const toggleEdit = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<template>
<app-dialog v-model="model" width="344px" is-centered>
<div class="export-account-form">
<h3>Export private key</h3>

<span v-show="!isDone">
<h4>Enter Extension password</h4>

<base-input
type="password"
placeholder="Extension Password"
class="export-account-form__input"
:value="keyringPassword"
@update:value="updateKeyringPassword"
@keyup.enter="showAction"
/>

<p v-show="keyringError" class="export-account-form__error">
Invalid Keyring password
</p>

<base-button
class="export-account-form__button"
title="Show"
:click="showAction"
:disabled="isLoading"
/>
</span>

<div v-if="isDone" class="privkey">
<p class="warning">
⚠️ Keep your private key secure. Never share it with anyone.
</p>
<p class="title">Private Key:</p>
<span class="word">{{ privKey }}</span>
</div>
</div>
</app-dialog>
</template>

<script setup lang="ts">
import { PropType, ref, onMounted, computed } from 'vue';
import AppDialog from '@action/components/app-dialog/index.vue';
import BaseButton from '@action/components/base-button/index.vue';
import BaseInput from '@action/components/base-input/index.vue';
import { NodeType } from '@/types/provider';
import { EnkryptAccount } from '@enkryptcom/types';
import KeyRingBase from '@/libs/keyring/keyring';
import BackupState from '@/libs/backup-state';
import { sendToBackgroundFromAction } from '@/libs/messenger/extension';
import { InternalMethods } from '@/types/messenger';

const model = defineModel<boolean>();
const closeWindow = () => {
model.value = false;
};
const keyringError = ref(false);
const isDone = ref(false);
const isLoading = ref(false);
const keyringPassword = ref(__PREFILL_PASSWORD__!);
const privKey = ref('');
const keyringBase = new KeyRingBase();

const props = defineProps({
account: {
type: Object as PropType<EnkryptAccount>,
default: () => ({}),
},
});

const showAction = async () => {
try {
isLoading.value = true;
const res = await sendToBackgroundFromAction({
message: JSON.stringify({
method: InternalMethods.getPrivateKey,
params: [props.account, keyringPassword.value],
}),
});
if (res.error) {
throw res.error;
} else {
privKey.value = JSON.parse(res.result!);
}
isDone.value = true;
} catch (err) {
keyringError.value = true;
}
isLoading.value = false;
};
Comment on lines +53 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid JSON round-trip and improve error handling UX.

  • Use the plain string result (after backend change).
  • Reset error state before attempting.
  • Provide a generic error message instead of always “Invalid Keyring password”.
 const showAction = async () => {
-  try {
-    isLoading.value = true;
+  try {
+    isLoading.value = true;
+    keyringError.value = false;
+    errorText.value = '';
     const res = await sendToBackgroundFromAction({
       message: JSON.stringify({
         method: InternalMethods.getPrivateKey,
         params: [props.account, keyringPassword.value],
       }),
     });
     if (res.error) {
       throw res.error;
     } else {
-      privKey.value = JSON.parse(res.result!);
+      privKey.value = res.result!;
     }
     isDone.value = true;
   } catch (err) {
-    keyringError.value = true;
+    keyringError.value = true;
+    errorText.value = 'Invalid password or export not allowed for this account.';
   }
   isLoading.value = false;
 };

Add this state in <script setup>:

-const keyringError = ref(false);
+const keyringError = ref(false);
+const errorText = ref('');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const model = defineModel<boolean>();
const closeWindow = () => {
model.value = false;
};
const keyringError = ref(false);
const isDone = ref(false);
const isLoading = ref(false);
const keyringPassword = ref(__PREFILL_PASSWORD__!);
const privKey = ref('');
const keyringBase = new KeyRingBase();
const props = defineProps({
account: {
type: Object as PropType<EnkryptAccount>,
default: () => ({}),
},
});
const showAction = async () => {
try {
isLoading.value = true;
const res = await sendToBackgroundFromAction({
message: JSON.stringify({
method: InternalMethods.getPrivateKey,
params: [props.account, keyringPassword.value],
}),
});
if (res.error) {
throw res.error;
} else {
privKey.value = JSON.parse(res.result!);
}
isDone.value = true;
} catch (err) {
keyringError.value = true;
}
isLoading.value = false;
};
// In <script setup>, add new errorText state next to keyringError
const keyringError = ref(false);
const errorText = ref('');
const isDone = ref(false);
const isLoading = ref(false);
const keyringPassword = ref(__PREFILL_PASSWORD__!);
const privKey = ref('');
const keyringBase = new KeyRingBase();
const showAction = async () => {
try {
isLoading.value = true;
keyringError.value = false;
errorText.value = '';
const res = await sendToBackgroundFromAction({
message: JSON.stringify({
method: InternalMethods.getPrivateKey,
params: [props.account, keyringPassword.value],
}),
});
if (res.error) {
throw res.error;
} else {
// backend now returns a plain string
privKey.value = res.result!;
}
isDone.value = true;
} catch (err) {
keyringError.value = true;
errorText.value = 'Invalid password or export not allowed for this account.';
} finally {
isLoading.value = false;
}
};


const updateKeyringPassword = (password: string) => {
keyringPassword.value = password;
};
</script>

<style lang="less" scoped>
@import '@action/styles/theme.less';
.export-account-form {
padding: 16px;

h3 {
font-style: normal;
font-weight: 700;
font-size: 24px;
line-height: 32px;
color: @primaryLabel;
margin: 0 0 16px 0;
}
&__button {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
width: 100%;
margin-top: 24px;
}
&__error {
width: 100%;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
text-align: center;
letter-spacing: 0.25px;
color: @error;
}

.privkey {
width: 100%;
margin-top: 28px;
.title {
font-style: normal;
font-weight: bold;
font-size: 16px;
line-height: 24px;
color: @primaryLabel;
margin-bottom: 6px;
}

.word {
font-style: normal;
font-weight: 400;
font-size: 18px;
line-height: 24px;
color: black;
background: @lightBg;
border: 1px solid rgba(95, 99, 104, 0.1);
box-sizing: border-box;
border-radius: 10px;
padding: 10px 16px;
margin: 0px;
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: start;
text-wrap: auto;
user-select: all;
line-break: anywhere;
}
.warning {
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 24px;
color: @error;
margin: 0 0 12px 0;
}
}
}
</style>
20 changes: 20 additions & 0 deletions packages/extension/src/ui/action/views/accounts/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
:identicon-element="network.identicon"
:show-edit="true"
:deletable="account.walletType !== WalletType.mnemonic"
:exportable="true"
@action:rename="renameAccount(index)"
@action:delete="deleteAccount(index)"
@action:export="exportAccount(index)"
/>

<div v-if="displayInactive.length > 0" class="accounts__info">
Expand Down Expand Up @@ -100,6 +102,13 @@
:account="accountToDelete"
/>

<export-account-form
v-if="isExportAccount"
v-model="isExportAccount"
v-bind="$attrs"
:account="accountToExport"
/>

<import-account
v-bind="$attrs"
v-model="isImportAccount"
Expand All @@ -114,6 +123,7 @@ import AddAccount from '@action/icons/common/add-account.vue';
import AddAccountForm from './components/add-account-form.vue';
import RenameAccountForm from './components/rename-account-form.vue';
import DeleteAccountForm from './components/delete-account-form.vue';
import ExportAccountForm from './components/export-account-form.vue';
import AddHardwareAccount from '@action/icons/actions/add-hardware-account.vue';
import ImportAccountIcon from '@action/icons/actions/import-account-icon.vue';
import ImportAccount from '@action/views/import-account/index.vue';
Expand All @@ -134,6 +144,7 @@ const emit = defineEmits<{
}>();
const isAddAccount = ref(false);
const isRenameAccount = ref(false);
const isExportAccount = ref(false);
const isDeleteAccount = ref(false);
const isImportAccount = ref(false);
const hwWallet = new HWwallets();
Expand All @@ -154,6 +165,7 @@ const props = defineProps({
});
const accountToRename = ref<EnkryptAccount>();
const accountToDelete = ref<EnkryptAccount>();
const accountToExport = ref<EnkryptAccount>();

const close = () => {
props.toggle();
Expand Down Expand Up @@ -185,6 +197,14 @@ const renameAccount = (accountIdx: number) => {
}, 100);
};

const exportAccount = (accountIdx: number) => {
accountToExport.value = props.accountInfo.activeAccounts[accountIdx];
props.toggle();
setTimeout(() => {
isExportAccount.value = true;
}, 100);
};

const deleteAccount = (accountIdx: number) => {
accountToDelete.value = props.accountInfo.activeAccounts[accountIdx];
props.toggle();
Expand Down
24 changes: 24 additions & 0 deletions packages/keyring/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,30 @@ class KeyRing {
await this.#storage.set(configs.STORAGE_KEYS.KEY_INFO, existingKeys);
}

async getPrivateKey(
options: SignOptions,
keyringPassword: string,
): Promise<string> {
assert(
!Object.values(HWwalletType).includes(
options.walletType as unknown as HWwalletType,
),
Errors.KeyringErrors.CannotUseKeyring,
);
if (options.walletType === WalletType.privkey) {
const privkeys = await this.#getPrivateKeys(keyringPassword);
assert(privkeys[options.pathIndex.toString()], Errors.KeyringErrors.AddressDoesntExists);
return privkeys[options.pathIndex.toString()];
} else {
const mnemonic = await this.#getMnemonic(keyringPassword)
const keypair = await this.#signers[options.signerType].generate(
mnemonic,
pathParser(options.basePath, options.pathIndex, options.signerType),
);
return keypair.privateKey;
}
}

async #getPrivateKeys(
keyringPassword: string,
): Promise<Record<string, string>> {
Expand Down
Loading