encrypt secret keys and require password

This commit is contained in:
hzrd149 2023-02-17 13:46:25 -06:00
parent d734c67302
commit 09b80bdb4a
11 changed files with 93 additions and 1181 deletions

View File

@ -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)

View File

@ -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": []
}

View File

@ -13,8 +13,8 @@
- [x] Broadcast events
- [x] User tipping
- [x] Manage followers ( Contact List )
- [x] Relay management
- [ ] Profile management
- [ ] Relay management
- [ ] Image upload
- [ ] Reactions
- [ ] 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-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-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
## 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 notifications service that keeps track of read notifications. (show unread count in sidenav)
- 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)
- Add note embeds
- Add "repost" button that mentions the note

View File

@ -30,7 +30,6 @@
"webln": "^0.3.2"
},
"devDependencies": {
"@changesets/cli": "^2.26.0",
"@types/identicon.js": "^2.3.1",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",

View File

@ -1,5 +1,7 @@
import { useToast } from "@chakra-ui/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 { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
@ -19,11 +21,13 @@ export function useSigningContext() {
export const SigningProvider = ({ children }: { children: React.ReactNode }) => {
const toast = useToast();
const current = useSubject(accountService.current);
const requestSignature = useCallback(
async (draft: DraftNostrEvent) => {
try {
return await signingService.requestSignature(draft);
if (!current) throw new Error("no account");
return await signingService.requestSignature(draft, current);
} catch (e) {
if (e instanceof Error) {
toast({

View File

@ -5,7 +5,8 @@ export type Account = {
pubkey: string;
readonly?: boolean;
relays?: string[];
secKey?: string;
secKey?: ArrayBuffer;
iv?: Uint8Array;
useExtension?: boolean;
};

View File

@ -75,7 +75,9 @@ async function savePending() {
if (!draft) return;
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);
await results.onComplete;

View File

@ -78,7 +78,9 @@ class ClientRelayService {
const oldRelayUrls = this.relays.value.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
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);
await results.onComplete;

View File

@ -1,11 +1,70 @@
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
import accountService from "./account";
import { Account } from "./account";
import { signEvent, getEventHash, getPublicKey } from "nostr-tools";
import db from "./db";
class SigningService {
async requestSignature(draft: DraftNostrEvent) {
const account = accountService.current.value;
private async getSalt() {
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?.useExtension) {
if (window.nostr) {
@ -14,8 +73,9 @@ class SigningService {
return signed;
} else throw new Error("missing nostr extension");
} else if (account?.secKey) {
const tmpDraft = { ...draft, pubkey: getPublicKey(account.secKey) };
const signature = signEvent(tmpDraft, account.secKey);
const secKey = await this.decryptSecKey(account);
const tmpDraft = { ...draft, pubkey: getPublicKey(secKey) };
const signature = signEvent(tmpDraft, secKey);
const event: NostrEvent = {
...tmpDraft,
id: getEventHash(tmpDraft),

View File

@ -22,6 +22,7 @@ import { Bech32Prefix, normalizeToBech32, normalizeToHex } from "../../helpers/n
import accountService from "../../services/account";
import clientRelaysService from "../../services/client-relays";
import { generatePrivateKey, getPublicKey } from "nostr-tools";
import signingService from "../../services/signing";
export const LoginNsecView = () => {
const navigate = useNavigate();
@ -66,13 +67,14 @@ export const LoginNsecView = () => {
[setInputValue, setHexKey, setNpub, setError]
);
const handleSubmit: React.FormEventHandler<HTMLDivElement> = (e) => {
const handleSubmit: React.FormEventHandler<HTMLDivElement> = async (e) => {
e.preventDefault();
if (!hexKey) return;
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);
accountService.switchAccount(pubkey);
};

1157
yarn.lock

File diff suppressed because it is too large Load Diff