diff options
| author | Adam Soltys <[email protected]> | 2024-08-30 18:44:33 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-08-30 18:44:33 -0700 |
| commit | cb2edf24e48a49417ed6ec4a386e566470ff046c (patch) | |
| tree | 4c773eea453252ed7211c9b65bf1ef4a7f4c2670 /src/lib/nostr.ts | |
| parent | cdf0174a1a5d37b34c1078d6dcc87a5d7fb162ce (diff) | |
| download | coinos-ui-cb2edf24e48a49417ed6ec4a386e566470ff046c.tar.gz coinos-ui-cb2edf24e48a49417ed6ec4a386e566470ff046c.zip | |
typescript
Diffstat (limited to 'src/lib/nostr.ts')
| -rw-r--r-- | src/lib/nostr.ts | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/src/lib/nostr.ts b/src/lib/nostr.ts new file mode 100644 index 00000000..5ee5be6f --- /dev/null +++ b/src/lib/nostr.ts @@ -0,0 +1,199 @@ +import { browser } from "$app/environment"; +import { hexToUint8Array } from "uint8array-extras"; +import { get } from "svelte/store"; +import { Buffer } from "buffer"; +import { fail, wait, post, stretch } from "$lib/utils"; +import { + decrypted, + password as pw, + passwordPrompt, + pin, + loginRedirect, +} from "$lib/store"; +import { goto, invalidate } from "$app/navigation"; +import { + getPublicKey, + finalizeEvent, + getEventHash, + nip04, + nip19, + type EventTemplate, +} from "nostr-tools"; + +import { + encrypt as nip49encrypt, + decrypt as nip49decrypt, +} from "nostr-tools/nip49"; + +import { mnemonicToEntropy, entropyToMnemonic } from "@scure/bip39"; +import wordlist from "$lib/english"; +import { bech32m } from "@scure/base"; + +import { generateSeedWords, privateKeyFromSeedWords } from "nostr-tools/nip06"; +import { PUBLIC_COINOS_PUBKEY } from "$env/static/public"; + +let damus = "wss://relay.damus.io"; + +type User = { + [key: string]: any; +}; + +type EncryptParams = { + message: string; + recipient: User; + user: User; +}; + +const { encode, decode, toWords, fromWords } = bech32m; + +export let generate = async (user: any) => { + if (get(pin)?.length !== 6) return; + + let salt = crypto.getRandomValues(new Uint8Array(16)); + let mnemonic = generateSeedWords(); + let sk = privateKeyFromSeedWords(mnemonic); + + let bytes = new Uint8Array( + await crypto.subtle.encrypt( + { name: "AES-GCM", iv: new Uint8Array(16) }, + await stretch(await getPassword(), salt), + mnemonicToEntropy(mnemonic, wordlist) + ) + ); + + user.pubkey = getPublicKey(hexToUint8Array(sk)); + user.cipher = encode("en", toWords(bytes), 180); + user.salt = Buffer.from(salt).toString("hex"); +}; + +export let encrypt = async ({ message, recipient, user }: EncryptParams) => { + let sk = await getPrivateKey(user); + return nip04.encrypt(sk, recipient, message); +}; + +export let decrypt = async ({ event, user }) => { + let cache = get(decrypted); + try { + let { content, pubkey, id } = event; + if (cache[id]) return cache[id]; + if (pubkey === user.pubkey) pubkey = event.tags[0][1]; + + let message = await nip04.decrypt( + await getPrivateKey(user), + pubkey, + content + ); + + cache[id] = Buffer.from(message).toString("utf8"); + decrypted.set(cache); + + return cache[id]; + } catch (e) { + // console.log(e); + } +}; + +function typedArrayToBuffer(array) { + return array.buffer.slice( + array.byteOffset, + array.byteLength + array.byteOffset + ); +} + +export let getPrivateKey = async (user: User): Promise<Uint8Array> => { + let k; + if (browser) { + k = localStorage.getItem("nsec"); + if (k) { + return nip19.decode(k).data as Uint8Array; + } + } + + let { nsec, username, salt } = user; + + if (nsec) { + k = nip49decrypt(nsec, await getPassword()); + } else k = hexToUint8Array(privateKeyFromSeedWords(await getMnemonic(user))); + + localStorage.setItem("nsec", nip19.nsecEncode(k)); + return k; +}; + +export let getMnemonic = async (user: User) => { + let { cipher, username, salt } = user; + let mnemonic, key, seed, entropy, child, privkey; + + entropy = new Uint8Array( + await crypto.subtle.decrypt( + { name: "AES-GCM", iv: new Uint8Array(16) }, + await stretch(await getPassword(), Buffer.from(salt, "hex")), + Uint8Array.from(fromWords(decode(cipher, 180).words)) + ) + ); + + return entropyToMnemonic(entropy, wordlist); +}; + +let decodeNsec = (nsec: string): Uint8Array => { + let { type, data } = nip19.decode(nsec); + if (type === "nsec" && data instanceof Uint8Array) return data; + else throw new Error("invalid nsec"); +}; + +export let getNsec = async (user: User) => { + return nip19.nsecEncode(await getPrivateKey(user)); +}; + +export let setNsec = async (user: User, nsec: string) => { + user.pubkey = getPublicKey(decodeNsec(nsec)); + user.nsec = await encryptNsec(nsec); +}; + +export let encryptNsec = async (nsec: string) => { + let d = decodeNsec(nsec); + return nip49encrypt(d, await getPassword()); +}; + +type SignParams = { + event: EventTemplate; + user: User; +}; + +export let sign = async ({ event, user }: SignParams) => { + event = finalizeEvent(event, await getPrivateKey(user)); +}; + +export let send = (event: EventTemplate) => { + return post("/events", { event }); +}; + +let getPassword = async () => { + if (!get(pw)) passwordPrompt.set(true); + await wait(() => !!get(pw)); + return get(pw); +}; + +export let reEncryptEntropy = async (user: User, newPassword: string) => { + let { cipher, username, salt } = user; + + let mnemonic, key, seed, entropy, child, privkey; + entropy = await crypto.subtle.decrypt( + { name: "AES-GCM", iv: new Uint8Array(16) }, + await stretch(await getPassword(), hexToUint8Array(salt)), + Uint8Array.from(fromWords(decode(cipher, 180).words)) + ); + + let bytes = new Uint8Array( + await crypto.subtle.encrypt( + { name: "AES-GCM", iv: new Uint8Array(16) }, + await stretch(newPassword, hexToUint8Array(salt)), + entropy + ) + ); + + return encode("en", toWords(bytes), 180); +}; + +export let nwc = (user: User) => { + `nostr+walletconnect://${PUBLIC_COINOS_PUBKEY}?relay=${damus}&secret=${user.nwc}`; +}; |
