diff --git a/src/extension/background-script/actions/nostr/decryptOrPrompt.ts b/src/extension/background-script/actions/nostr/decryptOrPrompt.ts new file mode 100644 index 0000000000..183e9d8f5d --- /dev/null +++ b/src/extension/background-script/actions/nostr/decryptOrPrompt.ts @@ -0,0 +1,35 @@ +import state from "~/extension/background-script/state"; +import { MessageDecryptGet } from "~/types"; + +const decryptOrPrompt = async (message: MessageDecryptGet) => { + if (!("host" in message.origin)) { + console.error("error", message.origin); + return; + } + + const result = await prompt(message); + + return result; +}; + +const prompt = async (message: MessageDecryptGet) => { + try { + // TODO: Add prompt & permissions + + const response = { + data: state + .getState() + .getNostr() + .decrypt(message.args.peer, message.args.ciphertext), + }; + + return response; + } catch (e) { + console.error("decrypt failed", e); + if (e instanceof Error) { + return { error: e.message }; + } + } +}; + +export default decryptOrPrompt; diff --git a/src/extension/background-script/actions/nostr/encryptOrPrompt.ts b/src/extension/background-script/actions/nostr/encryptOrPrompt.ts new file mode 100644 index 0000000000..4132fa9a5f --- /dev/null +++ b/src/extension/background-script/actions/nostr/encryptOrPrompt.ts @@ -0,0 +1,35 @@ +import state from "~/extension/background-script/state"; +import { MessageEncryptGet } from "~/types"; + +const encryptOrPrompt = async (message: MessageEncryptGet) => { + if (!("host" in message.origin)) { + console.error("error", message.origin); + return; + } + + const result = await prompt(message); + + return result; +}; + +const prompt = async (message: MessageEncryptGet) => { + try { + // TODO: Add prompt & permissions + + const response = { + data: state + .getState() + .getNostr() + .encrypt(message.args.peer, message.args.plaintext), + }; + + return response; + } catch (e) { + console.error("encrypt failed", e); + if (e instanceof Error) { + return { error: e.message }; + } + } +}; + +export default encryptOrPrompt; diff --git a/src/extension/background-script/actions/nostr/index.ts b/src/extension/background-script/actions/nostr/index.ts index 2c842649a5..65f4069733 100644 --- a/src/extension/background-script/actions/nostr/index.ts +++ b/src/extension/background-script/actions/nostr/index.ts @@ -1,3 +1,5 @@ +import decryptOrPrompt from "./decryptOrPrompt"; +import encryptOrPrompt from "./encryptOrPrompt"; import generatePrivateKey from "./generatePrivateKey"; import getPrivateKey from "./getPrivateKey"; import getPublicKeyOrPrompt from "./getPublicKeyOrPrompt"; @@ -12,4 +14,6 @@ export { getPublicKeyOrPrompt, getRelays, signEventOrPrompt, + encryptOrPrompt, + decryptOrPrompt, }; diff --git a/src/extension/background-script/nostr/__test__/nostr.test.ts b/src/extension/background-script/nostr/__test__/nostr.test.ts new file mode 100644 index 0000000000..61d918c9b6 --- /dev/null +++ b/src/extension/background-script/nostr/__test__/nostr.test.ts @@ -0,0 +1,29 @@ +import Nostr from "~/extension/background-script/nostr"; + +const alice = { + privateKey: + "9ab5b12ade1d9c27207ff0264e9fb155c77c9361c9b6a27c865fce1b2c0ddf0e", + publicKey: "0bf50e2fdc927853c12b64c06f6a703cfad8086e79b18b1eb864f3fab7fc6f74", +}; + +const bob = { + privateKey: + "b7eab8ab34aac491217a31059ec017e51c63d09c828e39ee3a40a016bc9d0cbf", + publicKey: "519f5ae2cd7d4b970c4edadb2efc947c9b803838de918d1c5bfd4b9c1a143b72", +}; + +describe("nostr", () => { + test("encrypt & decrypt", async () => { + const nostr = new Nostr(); + nostr.getPrivateKey = jest.fn().mockReturnValue(alice.privateKey); + + const message = "Secret message that is sent from Alice to Bob"; + const encrypted = nostr.encrypt(bob.publicKey, message); + + nostr.getPrivateKey = jest.fn().mockReturnValue(bob.privateKey); + + const decrypted = nostr.decrypt(alice.publicKey, encrypted); + + expect(decrypted).toMatch(message); + }); +}); diff --git a/src/extension/background-script/nostr/index.ts b/src/extension/background-script/nostr/index.ts index 271da512c3..a94e37098f 100644 --- a/src/extension/background-script/nostr/index.ts +++ b/src/extension/background-script/nostr/index.ts @@ -1,4 +1,10 @@ import * as secp256k1 from "@noble/secp256k1"; +import { Buffer } from "buffer"; +import { AES } from "crypto-js"; +import * as CryptoJS 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"; @@ -36,6 +42,35 @@ class Nostr { event.sig = signature; return event; } + + encrypt(pubkey: string, text: string) { + const key = secp256k1.getSharedSecret(this.getPrivateKey(), "02" + pubkey); + const normalizedKey = Buffer.from(key.slice(1, 33)); + const hexNormalizedKey = secp256k1.utils.bytesToHex(normalizedKey); + const hexKey = Hex.parse(hexNormalizedKey); + + const encrypted = AES.encrypt(text, hexKey, { + iv: CryptoJS.lib.WordArray.random(16), + }); + + return `${encrypted.toString()}?iv=${encrypted.iv.toString( + CryptoJS.enc.Base64 + )}`; + } + + decrypt(pubkey: string, ciphertext: string) { + const [cip, iv] = ciphertext.split("?iv="); + const key = secp256k1.getSharedSecret(this.getPrivateKey(), "02" + pubkey); + const normalizedKey = Buffer.from(key.slice(1, 33)); + const hexNormalizedKey = secp256k1.utils.bytesToHex(normalizedKey); + const hexKey = Hex.parse(hexNormalizedKey); + + const decrypted = AES.decrypt(cip, hexKey, { + iv: Base64.parse(iv), + }); + + return Utf8.stringify(decrypted); + } } export default Nostr; diff --git a/src/extension/background-script/router.ts b/src/extension/background-script/router.ts index 284c4c4837..a2e8355e14 100644 --- a/src/extension/background-script/router.ts +++ b/src/extension/background-script/router.ts @@ -75,6 +75,8 @@ const routes = { getPublicKeyOrPrompt: nostr.getPublicKeyOrPrompt, signEventOrPrompt: nostr.signEventOrPrompt, getRelays: nostr.getRelays, + encryptOrPrompt: nostr.encryptOrPrompt, + decryptOrPrompt: nostr.decryptOrPrompt, }, }, }; diff --git a/src/extension/content-script/onendnostr.js b/src/extension/content-script/onendnostr.js index 32a416476e..3d13d83130 100644 --- a/src/extension/content-script/onendnostr.js +++ b/src/extension/content-script/onendnostr.js @@ -9,6 +9,8 @@ const nostrCalls = [ "nostr/getPublicKeyOrPrompt", "nostr/signEventOrPrompt", "nostr/getRelays", + "nostr/encryptOrPrompt", + "nostr/decryptOrPrompt", ]; let callActive = false; diff --git a/src/extension/ln/nostr/index.ts b/src/extension/ln/nostr/index.ts index 1a33e69a4d..f198fd75ba 100644 --- a/src/extension/ln/nostr/index.ts +++ b/src/extension/ln/nostr/index.ts @@ -68,10 +68,10 @@ class Nip04 { } async encrypt(peer: string, plaintext: string): Promise { - throw new Error("Nip04 is not yet implemented."); + return this.provider.execute("encryptOrPrompt", { peer, plaintext }); } async decrypt(peer: string, ciphertext: string): Promise { - throw new Error("Nip04 is not yet implemented."); + return this.provider.execute("decryptOrPrompt", { peer, ciphertext }); } } diff --git a/src/types.ts b/src/types.ts index 92b0d56f43..e7d1b98329 100644 --- a/src/types.ts +++ b/src/types.ts @@ -366,6 +366,22 @@ export interface MessageSignEvent extends MessageDefault { action: "signEvent"; } +export interface MessageEncryptGet extends MessageDefault { + args: { + peer: string; + plaintext: string; + }; + action: "encrypt"; +} + +export interface MessageDecryptGet extends MessageDefault { + args: { + peer: string; + ciphertext: string; + }; + action: "decrypt"; +} + export interface LNURLChannelServiceResponse { uri: string; // Remote node address of form node_key@ip_address:port_number callback: string; // a second-level URL which would initiate an OpenChannel message from target LN node