Skip to content

Commit

Permalink
feat: add NIP 44 encryption support
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonathan Staab committed Dec 22, 2023
1 parent 608ec8b commit 404cf04
Show file tree
Hide file tree
Showing 29 changed files with 418 additions and 64 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"lodash.merge": "^4.6.2",
"lodash.pick": "^4.4.0",
"lodash.snakecase": "^4.1.1",
"nostr-tools": "^2.1.0",
"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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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.payload
);

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

// 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.payload
);

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,74 @@
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 { 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,
message.args.v
);
return { data: response };
} else {
const promptResponse = await utils.openPrompt<{
confirm: boolean;
rememberPermission: boolean;
}>({
...message,
action: "public/nostr/confirmEncryptOrDecrypt",
args: {
encryptOrDecrypt: {
action: "encrypt",
peer: message.args.peer,
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,
message.args.v
);

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);
});
});
31 changes: 28 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(pk: string) {
let key = this.nip44SharedSecretCache.get(pk);

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

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

return key;
}

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

nip44Encrypt(pubkey: string, text: string, v = 2) {
if (v !== 2) {
throw new Error("NIP44: unknown encryption version");
}

return nip44.v2.encrypt(text, this.getNip44SharedSecret(pubkey));
}

nip44Decrypt(pubkey: string, payload: string) {
return nip44.v2.decrypt(payload, this.getNip44SharedSecret(pubkey));
}

getEventHash(event: Event) {
return getEventHash(event);
}
Expand Down
23 changes: 23 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,25 @@ class Nip04 {
});
}
}

class Nip44 {
provider: NostrProvider;

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

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

async decrypt(peer: string, payload: string) {
await this.provider.enable();
return this.provider.execute("nip44DecryptOrPrompt", { peer, payload });
}
}
10 changes: 6 additions & 4 deletions src/i18n/locales/cs/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -864,10 +864,12 @@
"invoice": "Vytvořit novou platební fakturu"
},
"nostr": {
"nip04encrypt": "Zašifrovat data",
"getpublickey": "Přečíst svůj veřejný klíč",
"nip04decrypt": "Dešifrovat data",
"signmessage": "Podepsat zprávu klíčem"
"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": {
"estimatefee": "Odhadnout sazbu poplatku a celkové poplatky za transakci",
Expand Down
10 changes: 6 additions & 4 deletions src/i18n/locales/da/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -732,10 +732,12 @@
},
"permissions": {
"nostr": {
"getpublickey": "Læs din offentlige nøgle",
"nip04encrypt": "Krypter data",
"nip04decrypt": "De-krypter data",
"signmessage": "Underskriv meddelelse med din nøgle"
"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": {
"bkpr-listbalances": "Liste over alle nuværende og tidligere kontosaldi",
Expand Down
10 changes: 6 additions & 4 deletions src/i18n/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1164,10 +1164,12 @@
"settleinvoice": "Begleiche eine akzeptierte Rechnung"
},
"nostr": {
"getpublickey": "Lese deinen öffentlichen Schlüssel",
"nip04decrypt": "Daten entschlüsseln",
"nip04encrypt": "Daten verschlüsseln",
"signmessage": "Unterschreibe deine Nachricht mit deinem Schlüssel"
"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": {
"connectpeer": "Stelle eine Verbindung zu einer Gegenstelle her",
Expand Down
Loading

0 comments on commit 404cf04

Please sign in to comment.