mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-04 16:37:00 +02:00
encrypt secret keys and require password
This commit is contained in:
@@ -1,8 +0,0 @@
|
|||||||
# Changesets
|
|
||||||
|
|
||||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
|
||||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
|
||||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
|
||||||
|
|
||||||
We have a quick list of common questions to get you started engaging with this project in
|
|
||||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
|
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
|
||||||
"changelog": "@changesets/cli/changelog",
|
|
||||||
"commit": false,
|
|
||||||
"fixed": [],
|
|
||||||
"linked": [],
|
|
||||||
"access": "restricted",
|
|
||||||
"baseBranch": "main",
|
|
||||||
"updateInternalDependencies": "patch",
|
|
||||||
"ignore": []
|
|
||||||
}
|
|
@@ -13,8 +13,8 @@
|
|||||||
- [x] Broadcast events
|
- [x] Broadcast events
|
||||||
- [x] User tipping
|
- [x] User tipping
|
||||||
- [x] Manage followers ( Contact List )
|
- [x] Manage followers ( Contact List )
|
||||||
|
- [x] Relay management
|
||||||
- [ ] Profile management
|
- [ ] Profile management
|
||||||
- [ ] Relay management
|
|
||||||
- [ ] Image upload
|
- [ ] Image upload
|
||||||
- [ ] Reactions
|
- [ ] Reactions
|
||||||
- [ ] Dynamically connect to relays (start with one relay then connect to others as required)
|
- [ ] Dynamically connect to relays (start with one relay then connect to others as required)
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
- [ ] [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md): Authentication of clients to relays
|
- [ ] [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md): Authentication of clients to relays
|
||||||
- [ ] [NIP-50](https://github.com/nostr-protocol/nips/blob/master/50.md): Keywords filter
|
- [ ] [NIP-50](https://github.com/nostr-protocol/nips/blob/master/50.md): Keywords filter
|
||||||
- [ ] [NIP-56](https://github.com/nostr-protocol/nips/blob/master/56.md): Reporting
|
- [ ] [NIP-56](https://github.com/nostr-protocol/nips/blob/master/56.md): Reporting
|
||||||
|
- [ ] [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md): Lightning Zaps
|
||||||
- [x] [NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md): Relay List Metadata
|
- [x] [NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md): Relay List Metadata
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
@@ -55,7 +56,6 @@
|
|||||||
- Create a "event posting" service that can show modals (for qr code scanning), warnings (signed by wrong pubkey), and results (what relays responded) when posting events.
|
- Create a "event posting" service that can show modals (for qr code scanning), warnings (signed by wrong pubkey), and results (what relays responded) when posting events.
|
||||||
- Create notifications service that keeps track of read notifications. (show unread count in sidenav)
|
- Create notifications service that keeps track of read notifications. (show unread count in sidenav)
|
||||||
- Rebuild relays view to show relay info and settings NIP-11
|
- Rebuild relays view to show relay info and settings NIP-11
|
||||||
- use `nostr-tools` to allow user to generate and use nsec keys for login.
|
|
||||||
- filter list of followers by users the user has blocked/reported (stops bots/spammers from showing up at followers)
|
- filter list of followers by users the user has blocked/reported (stops bots/spammers from showing up at followers)
|
||||||
- Add note embeds
|
- Add note embeds
|
||||||
- Add "repost" button that mentions the note
|
- Add "repost" button that mentions the note
|
||||||
|
@@ -30,7 +30,6 @@
|
|||||||
"webln": "^0.3.2"
|
"webln": "^0.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.26.0",
|
|
||||||
"@types/identicon.js": "^2.3.1",
|
"@types/identicon.js": "^2.3.1",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-dom": "^18.0.9",
|
"@types/react-dom": "^18.0.9",
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import { useToast } from "@chakra-ui/react";
|
import { useToast } from "@chakra-ui/react";
|
||||||
import React, { useCallback, useContext, useMemo } from "react";
|
import React, { useCallback, useContext, useMemo } from "react";
|
||||||
|
import useSubject from "../hooks/use-subject";
|
||||||
|
import accountService from "../services/account";
|
||||||
import signingService from "../services/signing";
|
import signingService from "../services/signing";
|
||||||
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||||
|
|
||||||
@@ -19,11 +21,13 @@ export function useSigningContext() {
|
|||||||
|
|
||||||
export const SigningProvider = ({ children }: { children: React.ReactNode }) => {
|
export const SigningProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const current = useSubject(accountService.current);
|
||||||
|
|
||||||
const requestSignature = useCallback(
|
const requestSignature = useCallback(
|
||||||
async (draft: DraftNostrEvent) => {
|
async (draft: DraftNostrEvent) => {
|
||||||
try {
|
try {
|
||||||
return await signingService.requestSignature(draft);
|
if (!current) throw new Error("no account");
|
||||||
|
return await signingService.requestSignature(draft, current);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
toast({
|
toast({
|
||||||
|
@@ -5,7 +5,8 @@ export type Account = {
|
|||||||
pubkey: string;
|
pubkey: string;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
relays?: string[];
|
relays?: string[];
|
||||||
secKey?: string;
|
secKey?: ArrayBuffer;
|
||||||
|
iv?: Uint8Array;
|
||||||
useExtension?: boolean;
|
useExtension?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -75,7 +75,9 @@ async function savePending() {
|
|||||||
if (!draft) return;
|
if (!draft) return;
|
||||||
|
|
||||||
savingDraft.next(true);
|
savingDraft.next(true);
|
||||||
const event = await signingService.requestSignature(draft);
|
const current = accountService.current.value;
|
||||||
|
if (!current) throw new Error("no account");
|
||||||
|
const event = await signingService.requestSignature(draft, current);
|
||||||
|
|
||||||
const results = nostrPostAction(clientRelaysService.getWriteUrls(), event);
|
const results = nostrPostAction(clientRelaysService.getWriteUrls(), event);
|
||||||
await results.onComplete;
|
await results.onComplete;
|
||||||
|
@@ -78,7 +78,9 @@ class ClientRelayService {
|
|||||||
const oldRelayUrls = this.relays.value.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
|
const oldRelayUrls = this.relays.value.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
|
||||||
const writeUrls = unique([...oldRelayUrls, ...newRelayUrls]);
|
const writeUrls = unique([...oldRelayUrls, ...newRelayUrls]);
|
||||||
|
|
||||||
const event = await signingService.requestSignature(draft);
|
const current = accountService.current.value;
|
||||||
|
if (!current) throw new Error("no account");
|
||||||
|
const event = await signingService.requestSignature(draft, current);
|
||||||
|
|
||||||
const results = nostrPostAction(writeUrls, event);
|
const results = nostrPostAction(writeUrls, event);
|
||||||
await results.onComplete;
|
await results.onComplete;
|
||||||
|
@@ -1,11 +1,70 @@
|
|||||||
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||||
import accountService from "./account";
|
import { Account } from "./account";
|
||||||
import { signEvent, getEventHash, getPublicKey } from "nostr-tools";
|
import { signEvent, getEventHash, getPublicKey } from "nostr-tools";
|
||||||
|
import db from "./db";
|
||||||
|
|
||||||
class SigningService {
|
class SigningService {
|
||||||
async requestSignature(draft: DraftNostrEvent) {
|
private async getSalt() {
|
||||||
const account = accountService.current.value;
|
let salt = await db.get("settings", "salt");
|
||||||
|
if (salt) {
|
||||||
|
return salt as Uint8Array;
|
||||||
|
} else {
|
||||||
|
const newSalt = window.crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
await db.put("settings", newSalt, "salt");
|
||||||
|
return newSalt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getKeyMaterial() {
|
||||||
|
const password = window.prompt("Enter local encryption password");
|
||||||
|
if (!password) throw new Error("password required");
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
return window.crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveBits", "deriveKey"]);
|
||||||
|
}
|
||||||
|
private async getEncryptionKey() {
|
||||||
|
const salt = await this.getSalt();
|
||||||
|
const keyMaterial = await this.getKeyMaterial();
|
||||||
|
return await window.crypto.subtle.deriveKey(
|
||||||
|
{
|
||||||
|
name: "PBKDF2",
|
||||||
|
salt,
|
||||||
|
iterations: 100000,
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
keyMaterial,
|
||||||
|
{ name: "AES-GCM", length: 256 },
|
||||||
|
true,
|
||||||
|
["encrypt", "decrypt"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encryptSecKey(secKey: string) {
|
||||||
|
const key = await this.getEncryptionKey();
|
||||||
|
const encode = new TextEncoder();
|
||||||
|
const iv = window.crypto.getRandomValues(new Uint8Array(96));
|
||||||
|
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encode.encode(secKey));
|
||||||
|
|
||||||
|
return {
|
||||||
|
secKey: encrypted,
|
||||||
|
iv,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptSecKey(account: Account) {
|
||||||
|
if (!account.secKey) throw new Error("account dose not have a secret key");
|
||||||
|
const key = await this.getEncryptionKey();
|
||||||
|
const decode = new TextDecoder();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decrypted = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv: account.iv }, key, account.secKey);
|
||||||
|
return decode.decode(decrypted);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("failed to decrypt secret key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestSignature(draft: DraftNostrEvent, account: Account) {
|
||||||
if (account?.readonly) throw new Error("cant sign in readonly mode");
|
if (account?.readonly) throw new Error("cant sign in readonly mode");
|
||||||
if (account?.useExtension) {
|
if (account?.useExtension) {
|
||||||
if (window.nostr) {
|
if (window.nostr) {
|
||||||
@@ -14,8 +73,9 @@ class SigningService {
|
|||||||
return signed;
|
return signed;
|
||||||
} else throw new Error("missing nostr extension");
|
} else throw new Error("missing nostr extension");
|
||||||
} else if (account?.secKey) {
|
} else if (account?.secKey) {
|
||||||
const tmpDraft = { ...draft, pubkey: getPublicKey(account.secKey) };
|
const secKey = await this.decryptSecKey(account);
|
||||||
const signature = signEvent(tmpDraft, account.secKey);
|
const tmpDraft = { ...draft, pubkey: getPublicKey(secKey) };
|
||||||
|
const signature = signEvent(tmpDraft, secKey);
|
||||||
const event: NostrEvent = {
|
const event: NostrEvent = {
|
||||||
...tmpDraft,
|
...tmpDraft,
|
||||||
id: getEventHash(tmpDraft),
|
id: getEventHash(tmpDraft),
|
||||||
|
@@ -22,6 +22,7 @@ import { Bech32Prefix, normalizeToBech32, normalizeToHex } from "../../helpers/n
|
|||||||
import accountService from "../../services/account";
|
import accountService from "../../services/account";
|
||||||
import clientRelaysService from "../../services/client-relays";
|
import clientRelaysService from "../../services/client-relays";
|
||||||
import { generatePrivateKey, getPublicKey } from "nostr-tools";
|
import { generatePrivateKey, getPublicKey } from "nostr-tools";
|
||||||
|
import signingService from "../../services/signing";
|
||||||
|
|
||||||
export const LoginNsecView = () => {
|
export const LoginNsecView = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -66,13 +67,14 @@ export const LoginNsecView = () => {
|
|||||||
[setInputValue, setHexKey, setNpub, setError]
|
[setInputValue, setHexKey, setNpub, setError]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit: React.FormEventHandler<HTMLDivElement> = (e) => {
|
const handleSubmit: React.FormEventHandler<HTMLDivElement> = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!hexKey) return;
|
if (!hexKey) return;
|
||||||
const pubkey = getPublicKey(hexKey);
|
const pubkey = getPublicKey(hexKey);
|
||||||
|
|
||||||
accountService.addAccount({ pubkey, relays: [relayUrl], secKey: hexKey });
|
const encrypted = await signingService.encryptSecKey(hexKey);
|
||||||
|
accountService.addAccount({ pubkey, relays: [relayUrl], ...encrypted });
|
||||||
clientRelaysService.bootstrapRelays.add(relayUrl);
|
clientRelaysService.bootstrapRelays.add(relayUrl);
|
||||||
accountService.switchAccount(pubkey);
|
accountService.switchAccount(pubkey);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user