Skip to content

Commit

Permalink
Added encrypted dm and NIP-19 (npub and nsec parser) support.
Browse files Browse the repository at this point in the history
  • Loading branch information
KiPSOFT committed Jan 15, 2023
1 parent c55c469 commit b82e353
Show file tree
Hide file tree
Showing 9 changed files with 1,076 additions and 31 deletions.
Binary file added .DS_Store
Binary file not shown.
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Deno - https://deno.land/
- [x] Reply post.
- [x] Debug mode.
- [x] Promise-based simple and easy to use.
- [x] Encrypted send and receive direc messages.
- [x] npub and nsec prefix key support.
- [x] Async iterable filters.


### Usage
---
Expand All @@ -27,8 +31,8 @@ const nostr = new Nostr();
nostr.privateKey = ''; // A private key is optional. Only used for sending posts.

nostr.relayList.push({
name: 'Semisol',
url: 'wss://nostr-pub.semisol.dev'
name: 'Nostrprotocol',
url: 'wss://relay.nostrprotocol.net'
});

nostr.relayList.push({
Expand Down Expand Up @@ -60,24 +64,39 @@ const feeds = await nostr.globalFeed({
});
console.log('Feeds', feeds);

//method 1: for await
console.log('iterable return');
for await (const note of nostr.filter(filter) ) {
console.log(note);
}

//method 2: collect
console.log('promise return');
const allNotes = await nostr.filter(filter).collect();
console.log(allNotes);

//method 3: callback
console.log('callback return');
await nostr.filter(filter).each(note => {
console.log(note);
});

await nostr.disconnect();
console.log('Finish');
```

### Supported NIPs
---

NIP-01, NIP-02, NIP-05, NIP-08, NIP-10, NIP-20
NIP-01, NIP-02, NIP-04 NIP-05, NIP-08, NIP-10, NIP-19 NIP-20

### Roadmap
---

- [ ] Encrypted DMs.
- [ ] NIP-05 DNS-based internet identifier checking.
- [ ] Add user for follow.
- [ ] Public chat (channels).
- [ ] Hashtag list. NIP-12
- [ ] Filter posts with hashtag.
- [ ] CI for deno build.
- [ ] Split examples.

388 changes: 387 additions & 1 deletion deno.lock

Large diffs are not rendered by default.

17 changes: 6 additions & 11 deletions examples/basic.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { Nostr, Relay, NostrKind } from 'https://deno.land/x/[email protected]/mod.ts';
// import { Nostr, NostrKind } from "../nostr.ts";
// import { Nostr, Relay, NostrKind } from 'https://deno.land/x/[email protected]/mod.ts';
import { Nostr, Relay, NostrKind } from "../nostr.ts";

const nostr = new Nostr();

nostr.relayList.push({
name: 'Semisol',
url: 'wss://nostr-pub.semisol.dev'
});

nostr.relayList.push({
name: 'Wellorder',
url: 'wss://nostr-pub.wellorder.net'
name: 'Nostrprotocol',
url: 'wss://relay.nostrprotocol.net'
});

nostr.on('relayConnected', (relay: Relay) => console.log('Relay connected.', relay.name));
nostr.on('relayError', (err: Error) => console.log('Relay error;', err));
nostr.on('relayNotice', (notice: string[]) => console.log('Notice', notice));

//nostr.debugMode = true;
nostr.debugMode = true;

await nostr.connect();

Expand Down Expand Up @@ -59,4 +54,4 @@ const feeds = await nostr.globalFeed({
});
console.log('Feeds', feeds);

console.log('Finish');
console.log('Finish');
19 changes: 19 additions & 0 deletions examples/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Nostr, Relay } from "../nostr.ts";

const nostr = new Nostr();

nostr.relayList.push({
name: 'Nostrprotocol',
url: 'wss://relay.nostrprotocol.net'
});

nostr.on('relayConnected', (relay: Relay) => console.log('Relay connected.', relay.name));
nostr.on('relayError', (err: Error) => console.log('Relay error;', err));
nostr.on('relayNotice', (notice: string[]) => console.log('Notice', notice));

await nostr.connect();

nostr.privateKey = 'nsec***********************';

await nostr.sendMessage('npub*******************', 'Nostr Deno');
console.log(await nostr.getMessages());
73 changes: 73 additions & 0 deletions lib/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as secp from "https://deno.land/x/[email protected]/mod.ts";
import { base64 } from 'https://raw.githubusercontent.com/paulmillr/scure-base/main/mod.ts';

export default class Message {

private static getNormalizedX(key: Uint8Array): Uint8Array {
return key.slice(1, 33)
}

private static randomBytes(bytesLength: number = 32) {
return crypto.getRandomValues(new Uint8Array(bytesLength));
}

public static async encryptMessage(to: string, message: string, _privateKey: string): Promise<string> {
const key = secp.getSharedSecret(_privateKey, '02' + to);
const normalizedKey = this.getNormalizedX(key);
const encoder = new TextEncoder();
const iv = Uint8Array.from(this.randomBytes(16));
const plaintext = encoder.encode(message);
const cryptoKey = await crypto.subtle.importKey(
'raw',
normalizedKey,
{name: 'AES-CBC'},
false,
['encrypt']
);
const ciphertext = await crypto.subtle.encrypt(
{name: 'AES-CBC', iv},
cryptoKey,
plaintext
);
const toBase64 = (uInt8Array: Uint8Array) => btoa(String.fromCharCode(...uInt8Array));
const ctb64 = toBase64(new Uint8Array(ciphertext));
const ivb64 = toBase64(new Uint8Array(iv.buffer));
return `${ctb64}?iv=${ivb64}`;
}

private static stringToBuffer(value: string): ArrayBuffer {
let buffer = new ArrayBuffer(value.length * 2); // 2 bytes per char
let view = new Uint16Array(buffer);
for (let i = 0, length = value.length; i < length; i++) {
view[i] = value.charCodeAt(i);
}
return buffer;
}

public static async decrypt(privkey: string, pubkey: string, data: string): Promise<string> {
const [ ctb64, ivb64 ] = data.split('?iv=')
const key = secp.getSharedSecret(privkey, '02' + pubkey)
const normalizedKey = this.getNormalizedX(key)

const cryptoKey = await crypto.subtle.importKey(
'raw',
normalizedKey,
{name: 'AES-CBC'},
false,
['decrypt']
);
const ciphertext = base64.decode(ctb64);
const iv = base64.decode(ivb64);

const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv },
cryptoKey,
ciphertext
);

const txtDecode = new TextDecoder();
const text = txtDecode.decode(plaintext)
return text;
}

}
98 changes: 90 additions & 8 deletions lib/nostr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import EventEmitter from "https://deno.land/x/[email protected]/mod.ts";
import Relay, { NostrEvent } from "./relay.ts";
import * as secp from "https://deno.land/x/[email protected]/mod.ts";
import * as mod from "https://deno.land/[email protected]/encoding/hex.ts";
import Message from "./message.ts";
import { bech32 } from 'https://raw.githubusercontent.com/paulmillr/scure-base/main/mod.ts';

export enum NostrKind {
META_DATA = 0,
TEXT_NOTE = 1,
RECOMMED_SERVER = 2,
CONTACTS = 3
CONTACTS = 3,
DIRECT_MESSAGE = 4
}

export interface RelayList {
Expand Down Expand Up @@ -53,6 +56,13 @@ interface NostrEvents {
'relayPost': (id: string, status: boolean, errorMessage: string, relay: Relay) => void;
}

export interface NostrMessage {
content: string;
sender: string;
receiver: string;
createdAt: number;
}

declare interface Nostr {
on<U extends keyof NostrEvents>(
event: U, listener: NostrEvents[U]
Expand All @@ -67,19 +77,35 @@ class Nostr extends EventEmitter {
public relayList: Array<RelayList> = [];
private relayInstances: Array<Relay> = [];
private _privateKey: any;
public publicKey: any;
private _publicKey: any;
public debugMode = false;

constructor() {
super();
}

public set privateKey(value: any) {
private getKeyFromNip19(key: string) {
const code = bech32.decode(key, 1500);
const data = new Uint8Array(bech32.fromWords(code.words));
return secp.utils.bytesToHex(data);
}

public set privateKey(value: string) {
if (value.substring(0, 4) === 'nsec') {
value = this.getKeyFromNip19(value);
}
const decoder = new TextDecoder();
if (value) {
this._privateKey = value;
this.publicKey = decoder.decode(mod.encode(secp.schnorr.getPublicKey(this._privateKey)));
this._publicKey = decoder.decode(mod.encode(secp.schnorr.getPublicKey(this._privateKey)));
}
}

public set publicKey(value: string) {
if (value.substring(0, 4) === 'npub') {
value = this.getKeyFromNip19(value);
}
this._publicKey = value;
}

async connect() {
Expand Down Expand Up @@ -178,7 +204,7 @@ class Nostr extends EventEmitter {
}

async getMyProfile(): Promise<ProfileInfo> {
return await this.getProfile(this.publicKey);
return await this.getProfile(this._publicKey);
}

async getOtherProfile(publicKey: string): Promise<ProfileInfo> {
Expand Down Expand Up @@ -284,12 +310,12 @@ class Nostr extends EventEmitter {
}

async getPosts() {
if (!this.publicKey) {
if (!this._publicKey) {
throw new Error('You must set a public key for getting your posts.');
}
const filters = {
kinds: [NostrKind.TEXT_NOTE],
authors: [this.publicKey]
authors: [this._publicKey]
} as NostrFilters;
const events = await this.filter(filters).collect();
const posts = [] as Array<NostrPost>;
Expand Down Expand Up @@ -374,7 +400,7 @@ class Nostr extends EventEmitter {
created_at: Math.floor(Date.now() / 1000),
id: '',
kind: NostrKind.TEXT_NOTE,
pubkey: this.publicKey,
pubkey: this._publicKey,
sig: '',
tags: []
};
Expand Down Expand Up @@ -420,6 +446,62 @@ class Nostr extends EventEmitter {
console.log('Debug:', ...args);
}
}

public async sendMessage(to: string, message: any) {
if (!this._privateKey) {
throw new Error('You must set a private key send to the message.');
}
if (to.substring(0, 4) === 'npub') {
to = this.getKeyFromNip19(to);
}
const encrypted = await Message.encryptMessage(to, message, this._privateKey);
const event: NostrEvent = {
id: '',
created_at: Math.floor(Date.now() / 1000),
kind: NostrKind.DIRECT_MESSAGE,
pubkey: this._publicKey,
tags: [['p', to]],
content: encrypted,
sig: ''
};
event.id = await this.calculateId(event);
event.sig = new TextDecoder().decode(mod.encode(await this.signId(event.id)));
for (const relay of this.relayInstances) {
try {
await relay.sendEvent(event);
} catch (err) {
console.error(`Send direct message event error; ${err.message} Relay name; ${relay.name}`);
}
}
}

public async getMessages(): Promise<NostrMessage[]> {
if (!this._privateKey) {
throw new Error('You must set a private key send to the message.');
}
const events = await this.filter({
kinds: [NostrKind.DIRECT_MESSAGE],
"#p": this._publicKey
}).collect();
const messages: NostrMessage[] = [];
for (const event of events) {
const sender= event.tags.find(([k, v]) => k === 'p' && v && v !== '')[1];
try {
const pubKey = sender === this._publicKey ? event.pubkey : sender;
const msg = await Message.decrypt(this._privateKey, pubKey, event.content);
messages.push({
content: msg,
sender,
receiver: event.pubkey,
createdAt: event.created_at
});
} catch (err) {
this.log('Decrypt error;', err.message);
}
}
return messages;
}

}

export {
Expand Down
1 change: 1 addition & 0 deletions lib/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ class Relay {
this.ws?.send(message);
});
}

}

export default Relay;
Loading

0 comments on commit b82e353

Please sign in to comment.