+
-
+
{Object.keys(accounts).map((accountId) => {
const account = accounts[accountId];
return (
-
+
navigate(`/accounts/${accountId}`)}
+ >
@@ -148,175 +60,14 @@ function AccountsScreen() {
|
-
-
+ |
+
|
);
})}
-
-
-
-
- {t("edit.title")}
-
-
-
-
-
-
-
-
-
-
- {t("export.title")}
-
-
-
-
- {loading && (
-
-
- {t("export.waiting")}
-
- )}
- {!loading && (
-
- {lndHubData.lnAddress && (
-
-
- {t("export.your_ln_address")}
-
- {lndHubData.lnAddress &&
{lndHubData.lnAddress}
}
-
- )}
-
-
-
- {t("export.tip_mobile")}
-
-
{t("export.scan_qr")}
-
-
-
-
-
-
-
-
-
- )}
-
);
diff --git a/src/app/screens/Publisher.tsx b/src/app/screens/Publishers/Show/index.tsx
similarity index 100%
rename from src/app/screens/Publisher.tsx
rename to src/app/screens/Publishers/Show/index.tsx
diff --git a/src/app/screens/Settings.tsx b/src/app/screens/Settings/index.tsx
similarity index 76%
rename from src/app/screens/Settings.tsx
rename to src/app/screens/Settings/index.tsx
index 6321a0b716..0c14b95d31 100644
--- a/src/app/screens/Settings.tsx
+++ b/src/app/screens/Settings/index.tsx
@@ -1,8 +1,4 @@
-import {
- CrossIcon,
- HiddenIcon,
- VisibleIcon,
-} from "@bitcoin-design/bitcoin-icons-react/outline";
+import { CrossIcon } from "@bitcoin-design/bitcoin-icons-react/outline";
import Button from "@components/Button";
import Container from "@components/Container";
import LocaleSwitcher from "@components/LocaleSwitcher/LocaleSwitcher";
@@ -10,18 +6,17 @@ import PasswordForm from "@components/PasswordForm";
import Setting from "@components/Setting";
import Input from "@components/form/Input";
import Select from "@components/form/Select";
-import TextField from "@components/form/TextField";
import Toggle from "@components/form/Toggle";
import { Html5Qrcode } from "html5-qrcode";
import type { FormEvent } from "react";
-import { useState, useEffect } from "react";
-import { useTranslation, Trans } from "react-i18next";
+import { useState } from "react";
+import { Trans, useTranslation } from "react-i18next";
import Modal from "react-modal";
+import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { useSettings } from "~/app/context/SettingsContext";
import { CURRENCIES } from "~/common/constants";
import msg from "~/common/lib/msg";
-import nostrlib from "~/common/lib/nostr";
const initialFormData = {
password: "",
@@ -31,24 +26,11 @@ const initialFormData = {
function Settings() {
const { t } = useTranslation("translation", { keyPrefix: "settings" });
const { isLoading, settings, updateSetting } = useSettings();
+ const navigate = useNavigate();
const [modalIsOpen, setModalIsOpen] = useState(false);
const [formData, setFormData] = useState(initialFormData);
- const [nostrPrivateKey, setNostrPrivateKey] = useState("");
- const [nostrPrivateKeyVisible, setNostrPrivateKeyVisible] = useState(false);
-
- const getPrivateKeyFromStorage = async () => {
- const priv = (await msg.request("nostr/getPrivateKey")) as string;
- if (priv) {
- setNostrPrivateKey(nostrlib.hexToNip19(priv, "nsec"));
- }
- };
-
- useEffect(() => {
- getPrivateKeyFromStorage().catch(console.error);
- }, []);
-
const [cameraPermissionsGranted, setCameraPermissionsGranted] =
useState(false);
@@ -56,26 +38,6 @@ function Settings() {
setModalIsOpen(false);
}
- async function saveNostrPrivateKey(nostrPrivateKey: string) {
- const result = await msg.request("nostr/getPrivateKey");
- const currentPrivateKey = result as unknown as string;
-
- if (nostrPrivateKey === currentPrivateKey) return;
-
- if (currentPrivateKey && !confirm(t("nostr.private_key.warning"))) {
- return;
- }
-
- await msg.request("nostr/setPrivateKey", {
- privateKey: nostrlib.normalizeToHex(nostrPrivateKey),
- });
-
- saveSetting({
- nostrEnabled: !!nostrPrivateKey,
- });
- toast.success(t("nostr.private_key.success"));
- }
-
async function updateAccountPassword(password: string) {
await msg.request("changePassword", {
password: formData.password,
@@ -289,7 +251,7 @@ function Settings() {
{t("personal_data.description")}
-
+
{!isLoading && (
@@ -370,14 +332,9 @@ function Settings() {
-
- {t("nostr.title")}
+
+ {t("nostr.title")}
+
,
- ]}
- />
- }
+ subtitle={t("nostr.private_key.subtitle")}
>
-
-
- {
- saveNostrPrivateKey(nostrPrivateKey);
- }}
- onChange={(event) => {
- setNostrPrivateKey(event.target.value);
- }}
- endAdornment={
-
- }
- />
-
- {!nostrPrivateKey && (
-
-
- )}
+
+
diff --git a/src/app/screens/connectors/ConnectCitadel/index.tsx b/src/app/screens/connectors/ConnectCitadel/index.tsx
index b0ab87f1b9..c1543b9a1e 100644
--- a/src/app/screens/connectors/ConnectCitadel/index.tsx
+++ b/src/app/screens/connectors/ConnectCitadel/index.tsx
@@ -7,7 +7,7 @@ import ConnectorForm from "@components/ConnectorForm";
import TextField from "@components/form/TextField";
import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast";
import { useState } from "react";
-import { useTranslation, Trans } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import msg from "~/common/lib/msg";
diff --git a/src/app/screens/connectors/ConnectEclair/index.tsx b/src/app/screens/connectors/ConnectEclair/index.tsx
index 6a292ea9bf..c285a0beef 100644
--- a/src/app/screens/connectors/ConnectEclair/index.tsx
+++ b/src/app/screens/connectors/ConnectEclair/index.tsx
@@ -6,7 +6,7 @@ import ConnectorForm from "@components/ConnectorForm";
import TextField from "@components/form/TextField";
import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast";
import { useState } from "react";
-import { useTranslation, Trans } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import msg from "~/common/lib/msg";
diff --git a/src/app/screens/connectors/ConnectGaloy/index.tsx b/src/app/screens/connectors/ConnectGaloy/index.tsx
index 5c66fa81ee..596e15c2b6 100644
--- a/src/app/screens/connectors/ConnectGaloy/index.tsx
+++ b/src/app/screens/connectors/ConnectGaloy/index.tsx
@@ -3,7 +3,7 @@ import Input from "@components/form/Input";
import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast";
import axios from "axios";
import { useState } from "react";
-import { useTranslation, Trans } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import msg from "~/common/lib/msg";
diff --git a/src/app/screens/connectors/ConnectLnbits/index.tsx b/src/app/screens/connectors/ConnectLnbits/index.tsx
index e7e99e6a8c..8d0f5a4c24 100644
--- a/src/app/screens/connectors/ConnectLnbits/index.tsx
+++ b/src/app/screens/connectors/ConnectLnbits/index.tsx
@@ -3,7 +3,7 @@ import ConnectorForm from "@components/ConnectorForm";
import TextField from "@components/form/TextField";
import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast";
import { useState } from "react";
-import { useTranslation, Trans } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import msg from "~/common/lib/msg";
diff --git a/src/app/screens/connectors/ConnectMyNode/index.tsx b/src/app/screens/connectors/ConnectMyNode/index.tsx
index 8d19e4ee30..d2d89b29d2 100644
--- a/src/app/screens/connectors/ConnectMyNode/index.tsx
+++ b/src/app/screens/connectors/ConnectMyNode/index.tsx
@@ -3,7 +3,7 @@ import ConnectorForm from "@components/ConnectorForm";
import TextField from "@components/form/TextField";
import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast";
import { useState } from "react";
-import { useTranslation, Trans } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import msg from "~/common/lib/msg";
diff --git a/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx b/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx
index 5c9c158fe1..5eaad41e21 100644
--- a/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx
+++ b/src/app/screens/connectors/ConnectRaspiBlitz/index.tsx
@@ -3,7 +3,7 @@ import ConnectorForm from "@components/ConnectorForm";
import TextField from "@components/form/TextField";
import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast";
import { useState } from "react";
-import { useTranslation, Trans } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import msg from "~/common/lib/msg";
diff --git a/src/app/screens/connectors/ConnectStart9/index.tsx b/src/app/screens/connectors/ConnectStart9/index.tsx
index 45c1106ef4..02c346b248 100644
--- a/src/app/screens/connectors/ConnectStart9/index.tsx
+++ b/src/app/screens/connectors/ConnectStart9/index.tsx
@@ -3,7 +3,7 @@ import ConnectorForm from "@components/ConnectorForm";
import TextField from "@components/form/TextField";
import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast";
import { useState } from "react";
-import { useTranslation, Trans } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import msg from "~/common/lib/msg";
diff --git a/src/app/screens/connectors/ConnectUmbrel/index.tsx b/src/app/screens/connectors/ConnectUmbrel/index.tsx
index f27fa59922..ee703d51e5 100644
--- a/src/app/screens/connectors/ConnectUmbrel/index.tsx
+++ b/src/app/screens/connectors/ConnectUmbrel/index.tsx
@@ -3,7 +3,7 @@ import ConnectorForm from "@components/ConnectorForm";
import TextField from "@components/form/TextField";
import ConnectionErrorToast from "@components/toasts/ConnectionErrorToast";
import { useState } from "react";
-import { useTranslation, Trans } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import msg from "~/common/lib/msg";
diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts
index 246b604ee5..0c77c2a1bd 100644
--- a/src/common/lib/api.ts
+++ b/src/common/lib/api.ts
@@ -6,6 +6,7 @@ import {
MakeInvoiceResponse,
} from "~/extension/background-script/connectors/connector.interface";
import type {
+ Account,
AccountInfo,
Accounts,
Allowance,
@@ -33,6 +34,10 @@ export interface AccountInfoRes {
name: string;
}
+export interface GetAccountRes
+ extends Pick {
+ nostrEnabled: boolean;
+}
interface StatusRes {
configured: boolean;
unlocked: boolean;
@@ -92,6 +97,7 @@ export const swrGetAccountInfo = async (
});
};
export const getAccounts = () => msg.request("getAccounts");
+export const getAccount = () => msg.request("getAccount");
export const updateAllowance = () => msg.request("updateAllowance");
export const selectAccount = (id: string) =>
msg.request("selectAccount", { id });
@@ -130,6 +136,7 @@ export const getCurrencyRate = async () =>
msg.request<{ rate: number }>("getCurrencyRate");
export default {
+ getAccount,
getAccountInfo,
getAccounts,
getInfo,
diff --git a/src/extension/background-script/actions/accounts/__tests__/add.test.ts b/src/extension/background-script/actions/accounts/__tests__/add.test.ts
index b245ddc854..d05ed55355 100644
--- a/src/extension/background-script/actions/accounts/__tests__/add.test.ts
+++ b/src/extension/background-script/actions/accounts/__tests__/add.test.ts
@@ -27,6 +27,7 @@ const message: MessageAccountAdd = {
connector: "lnd",
config: "123456config",
name: "purple",
+ nostrPrivateKey: "123456nostr",
},
origin: { internal: true },
prompt: true,
@@ -57,6 +58,7 @@ describe("add account to account-list", () => {
connector: "lnd",
config: "secret-config-string-42",
name: "purple",
+ nostrPrivateKey: "123456nostr",
},
},
});
@@ -77,11 +79,13 @@ describe("add account to account-list", () => {
config: "abc",
connector: "lnd",
name: "BLUE",
+ nostrPrivateKey: "123",
},
"666": {
config: "xyz",
connector: "lnd",
name: "GREEN",
+ nostrPrivateKey: "123",
},
},
};
@@ -102,9 +106,20 @@ describe("add account to account-list", () => {
connector: "lnd",
config: "secret-config-string-42",
name: "purple",
+ nostrPrivateKey: "123456nostr",
+ },
+ "666": {
+ config: "xyz",
+ connector: "lnd",
+ name: "GREEN",
+ nostrPrivateKey: "123",
+ },
+ "888": {
+ config: "abc",
+ connector: "lnd",
+ name: "BLUE",
+ nostrPrivateKey: "123",
},
- "666": { config: "xyz", connector: "lnd", name: "GREEN" },
- "888": { config: "abc", connector: "lnd", name: "BLUE" },
},
});
diff --git a/src/extension/background-script/actions/accounts/__tests__/get.test.ts b/src/extension/background-script/actions/accounts/__tests__/get.test.ts
new file mode 100644
index 0000000000..e70944e78c
--- /dev/null
+++ b/src/extension/background-script/actions/accounts/__tests__/get.test.ts
@@ -0,0 +1,87 @@
+import type { GetAccountRes } from "~/common/lib/api";
+import state from "~/extension/background-script/state";
+import type { MessageAccountGet } from "~/types";
+
+import getAccount from "../get";
+
+jest.mock("~/extension/background-script/state");
+
+const mockState = {
+ getConnector: () => ({
+ getInfo: () => Promise.resolve({ data: { alias: "getalby.com" } }),
+ getBalance: () => Promise.resolve({ data: { balance: 0 } }),
+ }),
+ getAccount: () => ({
+ config:
+ "U2FsdGVkX19YMFK/8YpN5XQbMsmbVmlOJgpZCIRlt25K6ur4EPp4XdRUQC7+ep/m1k8d2yy69QfuGpsgn2SZOv4DQaPsdYTTwjj0mibQG/dkJ9OCp88zXuMpconrmRu5w4uZWEvdg7p5GQfIYJCvTPLUq+1zH3iH0xX7GhlrlQ8=",
+ connector: "lndhub",
+ id: "1e1e8ea6-493e-480b-9855-303d37506e97",
+ name: "Alby",
+ }),
+ currentAccountId: "1e1e8ea6-493e-480b-9855-303d37506e97",
+ accounts: {
+ "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e": {
+ config: "config-123-456",
+ connector: "lndhub",
+ id: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e",
+ name: "Alby",
+ nostrPrivateKey: "nostr-123-456",
+ },
+ "1e1e8ea6-493e-480b-9855-303d37506e97": {
+ config: "config-123-456",
+ connector: "lndhub",
+ id: "1e1e8ea6-493e-480b-9855-303d37506e97",
+ name: "Alby",
+ },
+ },
+};
+
+describe("account info", () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("get current account info", async () => {
+ const message: MessageAccountGet = {
+ application: "LBE",
+ origin: { internal: true },
+ prompt: true,
+ action: "getAccount",
+ };
+
+ state.getState = jest.fn().mockReturnValue(mockState);
+
+ const result: GetAccountRes = {
+ id: "1e1e8ea6-493e-480b-9855-303d37506e97",
+ name: "Alby",
+ connector: "lndhub",
+ nostrEnabled: false,
+ };
+
+ expect(await getAccount(message)).toStrictEqual({
+ data: result,
+ });
+ });
+ test("get account info by id", async () => {
+ const message: MessageAccountGet = {
+ application: "LBE",
+ origin: { internal: true },
+ prompt: true,
+ args: { id: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e" },
+ action: "getAccount",
+ };
+
+ state.getState = jest.fn().mockReturnValue(mockState);
+
+ const result: GetAccountRes = {
+ id: "8b7f1dc6-ab87-4c6c-bca5-19fa8632731e",
+ name: "Alby",
+ connector: "lndhub",
+ nostrEnabled: true,
+ };
+
+ expect(await getAccount(message)).toStrictEqual({
+ data: result,
+ });
+ });
+});
diff --git a/src/extension/background-script/actions/accounts/get.ts b/src/extension/background-script/actions/accounts/get.ts
new file mode 100644
index 0000000000..d19cb2f238
--- /dev/null
+++ b/src/extension/background-script/actions/accounts/get.ts
@@ -0,0 +1,29 @@
+import state from "~/extension/background-script/state";
+import type { MessageAccountGet } from "~/types";
+
+const get = async (message: MessageAccountGet) => {
+ const id = message?.args?.id || state.getState().currentAccountId;
+
+ if (!id)
+ return {
+ error: "No account selected.",
+ };
+
+ const accounts = state.getState().accounts;
+ const account = accounts[id];
+
+ if (!account) return;
+
+ const result = {
+ id: account.id,
+ connector: account.connector,
+ name: account.name,
+ nostrEnabled: !!account.nostrPrivateKey,
+ };
+
+ return {
+ data: result,
+ };
+};
+
+export default get;
diff --git a/src/extension/background-script/actions/accounts/index.ts b/src/extension/background-script/actions/accounts/index.ts
index 29142f1621..e41ef2f8f9 100644
--- a/src/extension/background-script/actions/accounts/index.ts
+++ b/src/extension/background-script/actions/accounts/index.ts
@@ -2,10 +2,22 @@ import add from "./add";
import all from "./all";
import decryptedDetails from "./decryptedDetails";
import edit from "./edit";
+import get from "./get";
import info from "./info";
import lock from "./lock";
import remove from "./remove";
import select from "./select";
import unlock from "./unlock";
-export { all, unlock, lock, add, edit, select, info, remove, decryptedDetails };
+export {
+ all,
+ unlock,
+ lock,
+ add,
+ edit,
+ select,
+ info,
+ remove,
+ get,
+ decryptedDetails,
+};
diff --git a/src/extension/background-script/actions/accounts/select.ts b/src/extension/background-script/actions/accounts/select.ts
index 0fd44ef529..f64e55505b 100644
--- a/src/extension/background-script/actions/accounts/select.ts
+++ b/src/extension/background-script/actions/accounts/select.ts
@@ -14,6 +14,7 @@ const select = async (message: MessageAccountSelect) => {
state.setState({
account,
+ nostr: null, // reset memoized nostr instance
connector: null, // reset memoized connector
currentAccountId: accountId,
});
diff --git a/src/extension/background-script/actions/nostr/generatePrivateKey.ts b/src/extension/background-script/actions/nostr/generatePrivateKey.ts
index 753bbd8fb4..a7aac8ec25 100644
--- a/src/extension/background-script/actions/nostr/generatePrivateKey.ts
+++ b/src/extension/background-script/actions/nostr/generatePrivateKey.ts
@@ -1,10 +1,33 @@
import * as secp256k1 from "@noble/secp256k1";
import Hex from "crypto-js/enc-hex";
import sha512 from "crypto-js/sha512";
+import type { MessagePrivateKeyGenerate } from "~/types";
import state from "../../state";
-const generatePrivateKey = async () => {
+const generatePrivateKey = async (message: MessagePrivateKeyGenerate) => {
+ const type = message?.args?.type;
+
+ const privateKey =
+ type === "random" ? generateRandomKey() : await deriveKey();
+
+ if (privateKey)
+ return {
+ data: {
+ privateKey: secp256k1.utils.bytesToHex(privateKey),
+ },
+ };
+ else
+ return {
+ error: "Error generating private key.",
+ };
+};
+
+const generateRandomKey = () => {
+ return secp256k1.utils.randomPrivateKey();
+};
+
+const deriveKey = async () => {
const connector = await state.getState().getConnector();
try {
const response = await connector.signMessage({
@@ -23,12 +46,7 @@ const generatePrivateKey = async () => {
// Use SHA-512 to provide enough key material for secp256k1 (> 40 bytes)
const hash = sha512(keymaterial).toString(Hex);
const privateKey = secp256k1.utils.hashToPrivateKey(hash);
-
- return {
- data: {
- privateKey: secp256k1.utils.bytesToHex(privateKey),
- },
- };
+ return privateKey;
} catch (e) {
console.error(e);
}
diff --git a/src/extension/background-script/actions/nostr/getPrivateKey.ts b/src/extension/background-script/actions/nostr/getPrivateKey.ts
index cf093ae274..714a377c86 100644
--- a/src/extension/background-script/actions/nostr/getPrivateKey.ts
+++ b/src/extension/background-script/actions/nostr/getPrivateKey.ts
@@ -1,9 +1,30 @@
+import { decryptData } from "~/common/lib/crypto";
+import type { MessagePrivateKeyGet } from "~/types";
+
import state from "../../state";
-const getPrivateKey = async () => {
- const privateKey = state.getState().getNostr().getPrivateKey();
+const getPrivateKey = async (message: MessagePrivateKeyGet) => {
+ const id = message?.args?.id;
+
+ if (!id) {
+ return {
+ data: state.getState().getNostr().privateKey,
+ };
+ }
+
+ const accounts = state.getState().accounts;
+ if (Object.keys(accounts).includes(id)) {
+ const password = state.getState().password as string;
+ const account = accounts[id];
+ if (!account.nostrPrivateKey) return { data: null };
+ const privateKey = decryptData(account.nostrPrivateKey, password);
+ return {
+ data: privateKey,
+ };
+ }
+
return {
- data: privateKey,
+ error: "Account does not exist.",
};
};
diff --git a/src/extension/background-script/actions/nostr/setPrivateKey.ts b/src/extension/background-script/actions/nostr/setPrivateKey.ts
index c7fa543bc7..b6f475951a 100644
--- a/src/extension/background-script/actions/nostr/setPrivateKey.ts
+++ b/src/extension/background-script/actions/nostr/setPrivateKey.ts
@@ -1,10 +1,32 @@
+import { encryptData } from "~/common/lib/crypto";
import type { MessagePrivateKeySet } from "~/types";
import state from "../../state";
const setPrivateKey = async (message: MessagePrivateKeySet) => {
- await state.getState().getNostr().setPrivateKey(message.args.privateKey);
- return {};
+ const id = message.args?.id || state.getState().currentAccountId;
+
+ const password = state.getState().password as string;
+ const privateKey = message.args.privateKey;
+ const accounts = state.getState().accounts;
+
+ if (id && Object.keys(accounts).includes(id)) {
+ const account = accounts[id];
+ account.nostrPrivateKey = privateKey
+ ? encryptData(privateKey, password)
+ : null;
+ accounts[id] = account;
+ state.setState({ accounts });
+ await state.getState().saveToStorage();
+ return {
+ data: {
+ accountId: id,
+ },
+ };
+ }
+ return {
+ error: "No account selected.",
+ };
};
export default setPrivateKey;
diff --git a/src/extension/background-script/actions/settings/changePassword.ts b/src/extension/background-script/actions/settings/changePassword.ts
index 28d93d59e9..919afca8cb 100644
--- a/src/extension/background-script/actions/settings/changePassword.ts
+++ b/src/extension/background-script/actions/settings/changePassword.ts
@@ -8,7 +8,6 @@ const changePassword = async (message: Message) => {
const password = state.getState().password as string;
const newPassword = message.args.password as string;
const tmpAccounts = { ...accounts };
- const nostPrivateKey = await state.getState().getNostr().getPrivateKey();
for (const accountId in tmpAccounts) {
const accountConfig = decryptData(
@@ -16,9 +15,16 @@ const changePassword = async (message: Message) => {
password
);
tmpAccounts[accountId].config = encryptData(accountConfig, newPassword);
+ const accountNostrKey = decryptData(
+ accounts[accountId].nostrPrivateKey as string,
+ password
+ );
+ tmpAccounts[accountId].nostrPrivateKey = encryptData(
+ accountNostrKey,
+ newPassword
+ );
}
state.setState({ accounts: tmpAccounts, password: newPassword });
- await state.getState().getNostr().setPrivateKey(nostPrivateKey);
// make sure we immediately persist the updated accounts
await state.getState().saveToStorage();
diff --git a/src/extension/background-script/migrations/index.ts b/src/extension/background-script/migrations/index.ts
index ddec35b1dc..81443b33d6 100644
--- a/src/extension/background-script/migrations/index.ts
+++ b/src/extension/background-script/migrations/index.ts
@@ -52,6 +52,19 @@ const migrations = {
// state is saved with the setMigrated call
}
},
+ migrateisUsingGlobalNostrKey: async () => {
+ const { nostrPrivateKey, accounts } = state.getState();
+
+ if (nostrPrivateKey) {
+ Object.values(accounts).map((account) => {
+ if (!account.nostrPrivateKey) account.nostrPrivateKey = nostrPrivateKey;
+ });
+
+ state.setState({
+ accounts,
+ });
+ }
+ },
};
const migrate = async () => {
@@ -64,6 +77,11 @@ const migrate = async () => {
await migrations["migrateisUsingLegacyLnurlAuthKeySetting"]();
await setMigrated("migrateisUsingLegacyLnurlAuthKeySetting");
}
+ if (shouldMigrate("migrateisUsingGlobalNostrKey")) {
+ console.info("Running migration for: migrateisUsingGlobalNostrKey");
+ await migrations["migrateisUsingGlobalNostrKey"]();
+ await setMigrated("migrateisUsingGlobalNostrKey");
+ }
if (shouldMigrate("migratedeleteLegacyWeblnPermissions")) {
console.info("Running migration for: migratedeleteLegacyWeblnPermissions");
await migrations["migratedeleteLegacyWeblnPermissions"]();
diff --git a/src/extension/background-script/nostr/__test__/nostr.test.ts b/src/extension/background-script/nostr/__test__/nostr.test.ts
index 61d918c9b6..33ec724609 100644
--- a/src/extension/background-script/nostr/__test__/nostr.test.ts
+++ b/src/extension/background-script/nostr/__test__/nostr.test.ts
@@ -12,18 +12,41 @@ const bob = {
publicKey: "519f5ae2cd7d4b970c4edadb2efc947c9b803838de918d1c5bfd4b9c1a143b72",
};
+const carol = {
+ privateKey:
+ "43a2d71f40dde6fb7588e7962a54b8bbd8dd4bd617a9a5c58b7bf0d8f3482f11",
+ publicKey: "a8c7d70a7d2e2826ce519a0a490fb953464c9d130235c321282983cd73be333f",
+};
+
describe("nostr", () => {
test("encrypt & decrypt", async () => {
- const nostr = new Nostr();
- nostr.getPrivateKey = jest.fn().mockReturnValue(alice.privateKey);
+ const aliceNostr = new Nostr(alice.privateKey);
const message = "Secret message that is sent from Alice to Bob";
- const encrypted = nostr.encrypt(bob.publicKey, message);
+ const encrypted = aliceNostr.encrypt(bob.publicKey, message);
- nostr.getPrivateKey = jest.fn().mockReturnValue(bob.privateKey);
+ const bobNostr = new Nostr(bob.privateKey);
- const decrypted = nostr.decrypt(alice.publicKey, encrypted);
+ const decrypted = bobNostr.decrypt(alice.publicKey, encrypted);
expect(decrypted).toMatch(message);
});
+
+ test("Carol can't decrypt Alice's message for Bob", async () => {
+ const aliceNostr = new Nostr(alice.privateKey);
+
+ const message = "Secret message that is sent from Alice to Bob";
+ const encrypted = aliceNostr.encrypt(bob.publicKey, message);
+
+ const carolNostr = new Nostr(carol.privateKey);
+
+ let decrypted;
+ try {
+ decrypted = carolNostr.decrypt(alice.publicKey, encrypted);
+ } catch (e) {
+ decrypted = "error decrypting message";
+ }
+
+ expect(decrypted).not.toMatch(message);
+ });
});
diff --git a/src/extension/background-script/nostr/index.ts b/src/extension/background-script/nostr/index.ts
index 4fb1bc9137..007b563e5a 100644
--- a/src/extension/background-script/nostr/index.ts
+++ b/src/extension/background-script/nostr/index.ts
@@ -5,51 +5,33 @@ import { AES } from "crypto-js";
import Base64 from "crypto-js/enc-base64";
import Hex from "crypto-js/enc-hex";
import Utf8 from "crypto-js/enc-utf8";
-import { decryptData, encryptData } from "~/common/lib/crypto";
import { Event } from "~/extension/ln/nostr/types";
-import { signEvent, getEventHash } from "../actions/nostr/helpers";
-import state from "../state";
+import { getEventHash, signEvent } from "../actions/nostr/helpers";
class Nostr {
- getPrivateKey() {
- const password = state.getState().password as string;
- const encryptedKey = state.getState().nostrPrivateKey as string;
- if (encryptedKey) {
- try {
- return decryptData(encryptedKey, password);
- } catch (e) {
- console.error("Could not decrypt the Nostr key");
- console.error(e);
- }
- }
+ privateKey: string;
- return null;
+ constructor(privateKey: string) {
+ this.privateKey = privateKey;
}
getPublicKey() {
const publicKey = secp256k1.schnorr.getPublicKey(
- secp256k1.utils.hexToBytes(this.getPrivateKey())
+ secp256k1.utils.hexToBytes(this.privateKey)
);
const publicKeyHex = secp256k1.utils.bytesToHex(publicKey);
return publicKeyHex;
}
- async setPrivateKey(privateKey: string) {
- const password = state.getState().password as string;
-
- state.setState({ nostrPrivateKey: encryptData(privateKey, password) });
- await state.getState().saveToStorage();
- }
-
async signEvent(event: Event): Promise {
- const signature = await signEvent(event, this.getPrivateKey());
+ const signature = await signEvent(event, this.privateKey);
event.sig = signature;
return event;
}
encrypt(pubkey: string, text: string) {
- const key = secp256k1.getSharedSecret(this.getPrivateKey(), "02" + pubkey);
+ const key = secp256k1.getSharedSecret(this.privateKey, "02" + pubkey);
const normalizedKey = Buffer.from(key.slice(1, 33));
const hexNormalizedKey = secp256k1.utils.bytesToHex(normalizedKey);
const hexKey = Hex.parse(hexNormalizedKey);
@@ -65,7 +47,7 @@ class Nostr {
decrypt(pubkey: string, ciphertext: string) {
const [cip, iv] = ciphertext.split("?iv=");
- const key = secp256k1.getSharedSecret(this.getPrivateKey(), "02" + pubkey);
+ const key = secp256k1.getSharedSecret(this.privateKey, "02" + pubkey);
const normalizedKey = Buffer.from(key.slice(1, 33));
const hexNormalizedKey = secp256k1.utils.bytesToHex(normalizedKey);
const hexKey = Hex.parse(hexNormalizedKey);
diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts
index 5dba8ab46f..7cdf113039 100644
--- a/src/extension/background-script/router.ts
+++ b/src/extension/background-script/router.ts
@@ -38,6 +38,7 @@ const routes = {
addAccount: accounts.add,
editAccount: accounts.edit,
getAccounts: accounts.all,
+ getAccount: accounts.get,
removeAccount: accounts.remove,
selectAccount: accounts.select,
setPassword: setup.setPassword,
diff --git a/src/extension/background-script/state.ts b/src/extension/background-script/state.ts
index 0d5d4adf15..c02b39f8db 100644
--- a/src/extension/background-script/state.ts
+++ b/src/extension/background-script/state.ts
@@ -110,8 +110,13 @@ const state = createState((set, get) => ({
if (get().nostr) {
return get().nostr as Nostr;
}
+ const currentAccountId = get().currentAccountId as string;
+ const account = get().accounts[currentAccountId];
+
+ const password = get().password as string;
+ const privateKey = decryptData(account.nostrPrivateKey as string, password);
- const nostr = new Nostr();
+ const nostr = new Nostr(privateKey);
set({ nostr: nostr });
return nostr;
@@ -121,7 +126,7 @@ const state = createState((set, get) => ({
if (connector) {
await connector.unload();
}
- set({ password: null, connector: null, account: null });
+ set({ password: null, connector: null, account: null, nostr: null });
},
isUnlocked: () => {
return get().password !== null;
diff --git a/src/extension/content-script/onstart.ts b/src/extension/content-script/onstart.ts
index b0154ae2fa..70e0a163cb 100644
--- a/src/extension/content-script/onstart.ts
+++ b/src/extension/content-script/onstart.ts
@@ -14,7 +14,7 @@ async function onstart() {
injectScript(browser.runtime.getURL("js/inpageScriptWebLN.bundle.js"));
// window.nostr
- const nostrEnabled = (await api.getSettings()).nostrEnabled;
+ const nostrEnabled = (await api.getAccount()).nostrEnabled;
if (nostrEnabled) {
injectScript(browser.runtime.getURL("js/inpageScriptNostr.bundle.js"));
}
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json
index e70c8014c1..d2e0a6f4cb 100644
--- a/src/i18n/locales/en/translation.json
+++ b/src/i18n/locales/en/translation.json
@@ -374,27 +374,61 @@
},
"accounts": {
"title": "Accounts",
- "actions": {
- "add_account": "Add account"
- },
- "edit": {
- "title": "Edit Account",
+ "account_view": {
+ "title1": "Account Information",
+ "title2": "Edit Account",
"name": {
- "label": "Name"
+ "title": "Name",
+ "placeholder": "Account Name"
+ },
+ "export": {
+ "title": "Export Account",
+ "screen_reader": "Export account details",
+ "waiting": "waiting for LndHub data...",
+ "your_ln_address": "Your Lightning Address:",
+ "tip_mobile": "Tip: Use this wallet with your mobile device",
+ "export_uri": "LNDHub Credentials URI",
+ "scan_qr": "Import this wallet into Zeus or BlueWallet by scanning the QRCode."
+ },
+ "nostr": {
+ "title": "Nostr",
+ "hint": "is a simple and open protocol that aims to create censorship-resistant social networks. Nostr works with cryptographic keys. To publish something you sign it with your key and send it to multiple relays. You can use Alby to manage your Nostr key. Many Nostr applications will then allow you to simply use the key from the Alby extension.",
+ "private_key": {
+ "title": "Manage your keys",
+ "subtitle": "Paste your private key or generate a new one. Make sure you backup your private key before you generate a new one. <0>Learn more »0>",
+ "warning": "This will delete your old private key. Are you sure?",
+ "success": "Private key encrypted & saved successfully.",
+ "successfully_removed": "Private key removed successfully.",
+ "label": "Private Key"
+ },
+ "public_key": {
+ "label": "Public Key"
+ },
+ "generate_keys": {
+ "title": "Generate new Nostr keys",
+ "screen_reader": "Generate new nostr keys for your account",
+ "hint": "You can generate a random set of Nostr keys or derive keys from this account details (using a signed canonical phrase). <0>Learn more »0>",
+ "actions": {
+ "random_keys": "Generate random keys",
+ "derived_keys": "Derive keys from account"
+ }
+ },
+ "actions": {
+ "generate": "Generate new keys"
+ }
},
- "screen_reader": "Edit account name"
- },
- "export": {
- "title": "Export Account",
- "screen_reader": "Export account details",
- "waiting": "waiting for LndHub data...",
- "your_ln_address": "Your Lightning Address:",
- "tip_mobile": "Tip: Use this wallet with your mobile device",
- "export_uri": "LNDHub Credentials URI",
- "scan_qr": "Import this wallet into Zeus or BlueWallet by scanning the QRCode."
+ "remove": {
+ "title": "Remove This Account",
+ "subtitle": "All the linked allowances will be deleted. Please be certain.",
+ "confirm": "Are you sure you want to remove account: {{name}}? \nThis can not be undone. If you used this account to login to websites you might lose access to those."
+ },
+ "actions": {
+ "remove_account": "Remove account",
+ "export": "Export"
+ }
},
- "remove": {
- "confirm": "Are you sure you want to remove account: {{name}}? \nThis can not be undone. If you used this account to login to websites you might lose access to those."
+ "actions": {
+ "add_account": "Add account"
}
},
"enable": {
@@ -512,10 +546,8 @@
"hint": "is a simple and open protocol that aims to create censorship-resistant social networks. Nostr works with cryptographic keys. To publish something you sign it with your key and send it to multiple relays. You can use Alby to manage your Nostr key. Many Nostr applications will then allow you to simply use the key from the Alby extension.",
"private_key": {
"title": "Private key",
- "subtitle": "Paste your private key or generate a new one. Generating a key will create a new key based on the current account you use. It can be re-generated, but please make sure you backup this private key. <0>Learn more »0>",
- "generate": "Generate",
- "warning": "This will delete your old private key. Are you sure?",
- "success": "Private key encrypted & saved successfully."
+ "subtitle": "This section is moved to accounts page as keys are account specific now.",
+ "go_to": "Go To Accounts"
}
}
},
@@ -704,6 +736,7 @@
"success": "Success",
"error": "Error",
"settings": "Settings",
+ "accounts": "Accounts",
"discover": "Discover",
"websites": "Websites",
"sats_one": "sat",
diff --git a/src/types.ts b/src/types.ts
index 51c752d90f..788478993b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -16,6 +16,7 @@ export interface Account {
connector: ConnectorType;
config: string;
name: string;
+ nostrPrivateKey?: string | null;
}
export interface Accounts {
@@ -166,6 +167,11 @@ export interface MessagePaymentAll extends MessageDefault {
};
}
+export interface MessageAccountGet extends MessageDefault {
+ args?: { id?: Account["id"] };
+ action: "getAccount";
+}
+
export interface MessageAccountRemove extends MessageDefault {
args?: { id: Account["id"] };
action: "removeAccount";
@@ -372,8 +378,23 @@ export interface MessagePublicKeyGet extends MessageDefault {
action: "getPublicKeyOrPrompt";
}
+export interface MessagePrivateKeyGet extends MessageDefault {
+ args?: {
+ id?: Account["id"];
+ };
+ action: "getPrivateKey";
+}
+
+export interface MessagePrivateKeyGenerate extends MessageDefault {
+ args?: {
+ type?: "random";
+ };
+ action: "generatePrivateKey";
+}
+
export interface MessagePrivateKeySet extends MessageDefault {
args: {
+ id?: Account["id"];
privateKey: string;
};
action: "setPrivateKey";