Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@
"@getalby/sdk": "^3.4.0",
"@headlessui/react": "^1.7.18",
"@lightninglabs/lnc-web": "^0.2.4-alpha",
"@noble/ciphers": "^0.5.1",
"@noble/curves": "^1.3.0",
"@noble/hashes": "^1.3.3",
"@noble/secp256k1": "^2.0.0",
"@popicons/react": "^0.0.9",
"@scure/base": "^1.1.5",
"@scure/bip32": "^1.3.3",
"@scure/bip39": "^1.2.2",
"@tailwindcss/forms": "^0.5.7",
Expand Down
33 changes: 33 additions & 0 deletions src/common/utils/lruCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export class LRUCache<T, U> {
map = new Map<T, U>();
keys: T[] = [];

constructor(readonly maxSize: number) {}

has(k: T) {
return this.map.has(k);
}

get(k: T) {
const v = this.map.get(k);

if (v !== undefined) {
this.keys.push(k as T);

if (this.keys.length > this.maxSize * 2) {
this.keys.splice(-this.maxSize);
}
}

return v;
}

set(k: T, v: U) {
this.map.set(k, v);
this.keys.push(k);

if (this.map.size > this.maxSize) {
this.map.delete(this.keys.shift() as T);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const decryptOrPrompt = async (message: MessageDecryptGet, sender: Sender) => {

if (hasPermission) {
const nostr = await state.getState().getNostr();
const response = await nostr.decrypt(
const response = await nostr.nip04Decrypt(
message.args.peer,
message.args.ciphertext
);
Expand All @@ -44,7 +44,7 @@ const decryptOrPrompt = async (message: MessageDecryptGet, sender: Sender) => {
}
if (promptResponse.data.confirm) {
const nostr = await state.getState().getNostr();
const response = await nostr.decrypt(
const response = await nostr.nip04Decrypt(
message.args.peer,
message.args.ciphertext
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const encryptOrPrompt = async (message: MessageEncryptGet, sender: Sender) => {
);

if (hasPermission) {
const response = (await state.getState().getNostr()).encrypt(
const nostr = await state.getState().getNostr();
const response = await nostr.nip04Encrypt(
message.args.peer,
message.args.plaintext
);
Expand Down Expand Up @@ -48,7 +49,8 @@ const encryptOrPrompt = async (message: MessageEncryptGet, sender: Sender) => {
);
}
if (promptResponse.data.confirm) {
const response = (await state.getState().getNostr()).encrypt(
const nostr = await state.getState().getNostr();
const response = await nostr.nip04Encrypt(
message.args.peer,
message.args.plaintext
);
Expand Down
4 changes: 4 additions & 0 deletions src/extension/background-script/actions/nostr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import getPrivateKey from "./getPrivateKey";
import getPublicKeyOrPrompt from "./getPublicKeyOrPrompt";
import getRelays from "./getRelays";
import isEnabled from "./isEnabled";
import nip44DecryptOrPrompt from "./nip44DecryptOrPrompt";
import nip44EncryptOrPrompt from "./nip44EncryptOrPrompt";
import removePrivateKey from "./removePrivateKey";
import setPrivateKey from "./setPrivateKey";
import signEventOrPrompt from "./signEventOrPrompt";
Expand All @@ -23,6 +25,8 @@ export {
getPublicKeyOrPrompt,
getRelays,
isEnabled,
nip44DecryptOrPrompt,
nip44EncryptOrPrompt,
removePrivateKey,
setPrivateKey,
signEventOrPrompt,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { USER_REJECTED_ERROR } from "~/common/constants";
import utils from "~/common/lib/utils";
import { getHostFromSender } from "~/common/utils/helpers";
import {
addPermissionFor,
hasPermissionFor,
} from "~/extension/background-script/permissions";
import state from "~/extension/background-script/state";
import { MessageNip44DecryptGet, PermissionMethodNostr, Sender } from "~/types";

const nip44DecryptOrPrompt = async (
message: MessageNip44DecryptGet,
sender: Sender
) => {
const host = getHostFromSender(sender);
if (!host) return;

try {
const hasPermission = await hasPermissionFor(
PermissionMethodNostr["NOSTR_NIP44DECRYPT"],
host
);

if (hasPermission) {
const nostr = await state.getState().getNostr();
const response = await nostr.nip44Decrypt(
message.args.peer,
message.args.ciphertext
);

return { data: response };
} else {
const promptResponse = await utils.openPrompt<{
confirm: boolean;
rememberPermission: boolean;
}>({
...message,
action: "public/nostr/confirmDecrypt",
});

// add permission to db only if user decided to always allow this request
if (promptResponse.data.rememberPermission) {
await addPermissionFor(
PermissionMethodNostr["NOSTR_NIP44DECRYPT"],
host
);
}
if (promptResponse.data.confirm) {
const nostr = await state.getState().getNostr();
const response = await nostr.nip44Decrypt(
message.args.peer,
message.args.ciphertext
);

return { data: response };
} else {
return { error: USER_REJECTED_ERROR };
}
}
} catch (e) {
console.error("decrypt failed", e);
if (e instanceof Error) {
return { error: e.message };
}
}
};

export default nip44DecryptOrPrompt;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { USER_REJECTED_ERROR } from "~/common/constants";
import nostr from "~/common/lib/nostr";
import utils from "~/common/lib/utils";
import { getHostFromSender } from "~/common/utils/helpers";
import {
addPermissionFor,
hasPermissionFor,
} from "~/extension/background-script/permissions";
import state from "~/extension/background-script/state";
import { MessageNip44EncryptGet, PermissionMethodNostr, Sender } from "~/types";

const nip44EncryptOrPrompt = async (
message: MessageNip44EncryptGet,
sender: Sender
) => {
const host = getHostFromSender(sender);
if (!host) return;

try {
const hasPermission = await hasPermissionFor(
PermissionMethodNostr["NOSTR_NIP44ENCRYPT"],
host
);

if (hasPermission) {
const response = (await state.getState().getNostr()).nip44Encrypt(
message.args.peer,
message.args.plaintext
);
return { data: response };
} else {
const promptResponse = await utils.openPrompt<{
confirm: boolean;
rememberPermission: boolean;
}>({
...message,
action: "public/nostr/confirmEncrypt",
args: {
encrypt: {
recipientNpub: nostr.hexToNip19(message.args.peer, "npub"),
message: message.args.plaintext,
},
},
});

// add permission to db only if user decided to always allow this request
if (promptResponse.data.rememberPermission) {
await addPermissionFor(
PermissionMethodNostr["NOSTR_NIP44ENCRYPT"],
host
);
}
if (promptResponse.data.confirm) {
const response = (await state.getState().getNostr()).nip44Encrypt(
message.args.peer,
message.args.plaintext
);

return { data: response };
} else {
return { error: USER_REJECTED_ERROR };
}
}
} catch (e) {
console.error("encrypt failed", e);
if (e instanceof Error) {
return { error: e.message };
}
}
};

export default nip44EncryptOrPrompt;
43 changes: 38 additions & 5 deletions src/extension/background-script/nostr/__test__/nostr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ const carol = {
publicKey: "a8c7d70a7d2e2826ce519a0a490fb953464c9d130235c321282983cd73be333f",
};

describe("nostr", () => {
describe("nostr.nip04", () => {
test("encrypt & decrypt", 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 encrypted = aliceNostr.nip04Encrypt(bob.publicKey, message);

const bobNostr = new Nostr(bob.privateKey);

const decrypted = await bobNostr.decrypt(alice.publicKey, encrypted);
const decrypted = await bobNostr.nip04Decrypt(alice.publicKey, encrypted);

expect(decrypted).toMatch(message);
});
Expand All @@ -36,13 +36,46 @@ describe("nostr", () => {
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 encrypted = aliceNostr.nip04Encrypt(bob.publicKey, message);

const carolNostr = new Nostr(carol.privateKey);

let decrypted;
try {
decrypted = await carolNostr.decrypt(alice.publicKey, encrypted);
decrypted = await carolNostr.nip04Decrypt(alice.publicKey, encrypted);
} catch (e) {
decrypted = "error decrypting message";
}

expect(decrypted).not.toMatch(message);
});
});

describe("nostr.nip44", () => {
test("encrypt & decrypt", async () => {
const aliceNostr = new Nostr(alice.privateKey);

const message = "Secret message that is sent from Alice to Bob";
const encrypted = aliceNostr.nip44Encrypt(bob.publicKey, message);

const bobNostr = new Nostr(bob.privateKey);

const decrypted = await bobNostr.nip44Decrypt(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.nip44Encrypt(bob.publicKey, message);

const carolNostr = new Nostr(carol.privateKey);

let decrypted;
try {
decrypted = await carolNostr.nip44Decrypt(alice.publicKey, encrypted);
} catch (e) {
decrypted = "error decrypting message";
}
Expand Down
31 changes: 26 additions & 5 deletions src/extension/background-script/nostr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,28 @@ 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 { LRUCache } from "~/common/utils/lruCache";
import { Event } from "~/extension/providers/nostr/types";
import { nip44 } from "./nip44";

import { getEventHash, signEvent } from "../actions/nostr/helpers";

class Nostr {
privateKey: string;
nip44SharedSecretCache = new LRUCache<string, Uint8Array>(100);

constructor(privateKey: string) {
this.privateKey = privateKey;
constructor(readonly privateKey: string) {}

// Deriving shared secret is an expensive computation
getNip44SharedSecret(peerPubkey: string) {
let key = this.nip44SharedSecretCache.get(peerPubkey);

if (!key) {
key = nip44.utils.getConversationKey(this.privateKey, peerPubkey);

this.nip44SharedSecretCache.set(peerPubkey, key);
}

return key;
}

getPublicKey() {
Expand All @@ -40,7 +53,7 @@ class Nostr {
return signedHex;
}

encrypt(pubkey: string, text: string) {
nip04Encrypt(pubkey: string, text: string) {
const key = secp256k1.getSharedSecret(this.privateKey, "02" + pubkey);
const normalizedKey = Buffer.from(key.slice(1, 33));
const hexNormalizedKey = secp256k1.etc.bytesToHex(normalizedKey);
Expand All @@ -55,7 +68,7 @@ class Nostr {
)}`;
}

async decrypt(pubkey: string, ciphertext: string) {
async nip04Decrypt(pubkey: string, ciphertext: string) {
const [cip, iv] = ciphertext.split("?iv=");
const key = secp256k1.getSharedSecret(this.privateKey, "02" + pubkey);
const normalizedKey = Buffer.from(key.slice(1, 33));
Expand All @@ -69,6 +82,14 @@ class Nostr {
return Utf8.stringify(decrypted);
}

nip44Encrypt(peer: string, plaintext: string) {
return nip44.encrypt(plaintext, this.getNip44SharedSecret(peer));
}

nip44Decrypt(peer: string, ciphertext: string) {
return nip44.decrypt(ciphertext, this.getNip44SharedSecret(peer));
}

getEventHash(event: Event) {
return getEventHash(event);
}
Expand Down
Loading