Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add NIP 44 encryption support #2653

Closed
wants to merge 9 commits into from
Closed
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"lodash.merge": "^4.6.2",
"lodash.pick": "^4.4.0",
"lodash.snakecase": "^4.1.1",
"nostr-tools": "^2.1.0",
staab marked this conversation as resolved.
Show resolved Hide resolved
"pubsub-js": "^1.9.4",
"react": "^18.2.0",
"react-confetti": "^6.1.0",
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);
}
}
}
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",
rolznz marked this conversation as resolved.
Show resolved Hide resolved
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;
35 changes: 34 additions & 1 deletion src/extension/background-script/nostr/__test__/nostr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const carol = {
publicKey: "a8c7d70a7d2e2826ce519a0a490fb953464c9d130235c321282983cd73be333f",
};

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

Expand Down Expand Up @@ -50,3 +50,36 @@ describe("nostr", () => {
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";
}

expect(decrypted).not.toMatch(message);
});
});
27 changes: 24 additions & 3 deletions src/extension/background-script/nostr/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import { schnorr } from "@noble/curves/secp256k1";
import * as secp256k1 from "@noble/secp256k1";
import { nip44 } from "nostr-tools";
import { Buffer } from "buffer";
import * as CryptoJS from "crypto-js";
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 { 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.v2.utils.getConversationKey(this.privateKey, peerPubkey);

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

return key;
}

getPublicKey() {
Expand Down Expand Up @@ -69,6 +82,14 @@ class Nostr {
return Utf8.stringify(decrypted);
}

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

nip44Decrypt(peer: string, ciphertext: string) {
return nip44.v2.decrypt(ciphertext, this.getNip44SharedSecret(peer));
}
staab marked this conversation as resolved.
Show resolved Hide resolved

getEventHash(event: Event) {
return getEventHash(event);
}
Expand Down
2 changes: 2 additions & 0 deletions src/extension/background-script/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ const routes = {
getRelays: nostr.getRelays,
encryptOrPrompt: nostr.encryptOrPrompt,
decryptOrPrompt: nostr.decryptOrPrompt,
nip44EncryptOrPrompt: nostr.nip44EncryptOrPrompt,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we rename the existing methods to be prefixed with nip04?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do that if you want, just let me know

nip44DecryptOrPrompt: nostr.nip44DecryptOrPrompt,
},
},
};
Expand Down
2 changes: 2 additions & 0 deletions src/extension/content-script/nostr.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const nostrCalls = [
"nostr/enable",
"nostr/encryptOrPrompt",
"nostr/decryptOrPrompt",
"nostr/nip44EncryptOrPrompt",
"nostr/nip44DecryptOrPrompt",
"nostr/on",
"nostr/off",
"nostr/emit",
Expand Down
25 changes: 25 additions & 0 deletions src/extension/providers/nostr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare global {

export default class NostrProvider extends ProviderBase {
nip04 = new Nip04(this);
nip44 = new Nip44(this);

constructor() {
super("nostr");
Expand Down Expand Up @@ -70,3 +71,27 @@ class Nip04 {
});
}
}

class Nip44 {
provider: NostrProvider;

constructor(provider: NostrProvider) {
this.provider = provider;
}

async encrypt(peer: string, plaintext: string) {
await this.provider.enable();
return this.provider.execute("nip44EncryptOrPrompt", {
peer,
plaintext,
});
}

async decrypt(peer: string, ciphertext: string) {
await this.provider.enable();
return this.provider.execute("nip44DecryptOrPrompt", {
peer,
ciphertext,
});
}
}
3 changes: 3 additions & 0 deletions src/i18n/locales/cs/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,9 @@
"nostr": {
"getpublickey": "Přečíst svůj veřejný klíč",
"nip04decrypt": "Dešifrovat data",
"nip04encrypt": "Zašifrovat data",
"nip44decrypt": "Dešifrovat data",
"nip44encrypt": "Zašifrovat data",
"signmessage": "Podepsat zprávu klíčem"
},
"lnc": {
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/da/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,9 @@
"nostr": {
"getpublickey": "Læs din offentlige nøgle",
"nip04decrypt": "De-krypter data",
"nip04encrypt": "Krypter data",
"nip44decrypt": "De-krypter data",
"nip44encrypt": "Krypter data",
"signmessage": "Underskriv meddelelse med din nøgle"
},
"commando": {
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,9 @@
"nostr": {
"getpublickey": "Lese deinen öffentlichen Schlüssel",
"nip04decrypt": "Daten entschlüsseln",
"nip04encrypt": "Daten verschlüsseln",
"nip44decrypt": "Daten entschlüsseln",
"nip44encrypt": "Daten verschlüsseln",
"signmessage": "Unterschreibe deine Nachricht mit deinem Schlüssel"
},
"lnc": {
Expand Down
Loading
Loading