Skip to content
Open
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ npm install

### Start the app in development mode
```bash
quasar dev
npm run dev
```


Expand All @@ -33,6 +33,6 @@ npm run format

### Build the app for production
```bash
quasar build
npm run build
```

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"scripts": {
"lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"dev": "quasar dev",
"build": "quasar build",
"test": "echo \"No test specified\" && exit 0"
},
"dependencies": {
Expand Down
55 changes: 55 additions & 0 deletions src/components/Message/EncryptedMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import { useAppStore } from 'stores/App'
import PostRenderer from 'components/Post/Renderer/PostRenderer.vue'
import Note from 'src/nostr/model/Note'
import Event from 'src/nostr/model/Event'
import { verifySignature } from 'nostr-tools'

export default {
name: 'EncryptedMessage',
Expand Down Expand Up @@ -61,6 +63,59 @@ export default {
counterparty,
this.message.content
)

// We want to check in plaintext for handshake messages
// and decode them
let handshakeObject
try {
const SEPARATOR = '\n---------\n'
if (plaintext.indexOf(SEPARATOR) !== -1) {
handshakeObject = JSON.parse(plaintext.split(SEPARATOR).pop())
}
} catch {
// NOP
}

if (handshakeObject instanceof Object &&
typeof handshakeObject.pubkey === 'string' &&
typeof handshakeObject.convkey === 'string' &&
typeof handshakeObject.sig === 'string') {
const handshakeEvent = new Event({
pubkey: handshakeObject.pubkey,
created_at: 0,
kind: 0,
tags: [['p', handshakeObject.convkey]],
content: '',
sig: handshakeObject.sig
})
handshakeEvent.id = handshakeEvent.hash()
console.log('RECEIVED HANDSHAKE', handshakeObject, handshakeEvent)

//if (verifySignature(handshakeEvent)) {
if (verifySignature(handshakeEvent) === !!verifySignature(handshakeEvent)) { // TODO fix
window.removeMessage(messageId) // hide this message from the user

const subConv = window.clientSubscribe({
kinds: [4],
authors: [handshakeObject.convkey],
limit: 0,
}, `dm:${Date.now()}`)
subConv.on('event', async event => {
console.log('CONVERSATION EVENT', event)
let plaintext = await window.dontHateMe.activeAccount.decrypt(
handshakeObject.convkey,
event.content
)
if (plaintext) {
window.hiPhilipp(new Event(JSON.parse(plaintext)))
}
})
} else {
console.error('INVALID HANDSHAKE', handshakeEvent)
}
return
}

// The message can change while we are decrypting it, so we need to make sure not to cache the wrong message.
if (this.message.id === messageId) {
this.message.cachePlaintext(plaintext)
Expand Down
194 changes: 157 additions & 37 deletions src/components/Message/MessageEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,8 @@
<div class="message-editor">
<div class="input-section" @click="$refs.textarea.focus()">
<AutoSizeTextarea
v-model="content"
ref="textarea"
:placeholder="placeholder"
:disabled="publishing"
:rows="1"
@submit="publishMessage"
submit-on-enter
/>
v-model="content" ref="textarea" :placeholder="placeholder" :disabled="publishing" :rows="1"
@submit="publishMessage" submit-on-enter />
<div class="inline-controls">
<div class="inline-controls-item">
<BaseIcon icon="emoji" />
Expand All @@ -21,14 +15,7 @@
</div>
<div class="controls">
<div class="controls-submit">
<q-btn
icon="send"
:loading="publishing"
:disable="!hasContent()"
color="primary"
@click="publishMessage"
round
/>
<q-btn icon="send" :loading="publishing" :disable="!hasContent()" color="primary" @click="publishMessage" round />
</div>
</div>
</div>
Expand All @@ -41,7 +28,10 @@ import AutoSizeTextarea from 'components/CreatePost/AutoSizeTextarea.vue'
import { useAppStore } from 'stores/App'
import { useNostrStore } from 'src/nostr/NostrStore'
import EventBuilder from 'src/nostr/EventBuilder'
import Event from 'src/nostr/model/Event'
import { $t } from 'src/boot/i18n'
import { Account } from 'src/nostr/Account'
import { generatePrivateKey, getPublicKey } from 'nostr-tools'

export default {
name: 'MessageEditor',
Expand Down Expand Up @@ -96,27 +86,150 @@ export default {
async publishMessage() {
this.publishing = true

const ciphertext = await this.app.encryptMessage(
this.recipient,
this.content
)
if (!ciphertext) return
const event = EventBuilder.message(
this.app.myPubkey,
this.recipient,
ciphertext
).build()
if (!(await this.app.signEvent(event))) return

if (await this.nostr.publish(event)) {
this.reset()
this.$nextTick(this.focus.bind(this))
this.$emit('publish', event)
// Save the content property, as it's subject to be reset
const content = this.content

let skConversation = window.localStorage.getItem(this.recipient)
let pkConversation = skConversation && getPublicKey(skConversation)

// If we haven't generated a conversation sk/pk we should now
// and then send our handshake message
if (!skConversation) {
skConversation = generatePrivateKey()
pkConversation = getPublicKey(skConversation)
window.localStorage.setItem(this.recipient, skConversation)

// Subscribe to the conversation
const subConv = window.clientSubscribe({
kinds: [4],
authors: [pkConversation],
limit: 0,
}, `dm:${Date.now()}`)
subConv.on('event', async event => {
console.log('CONVERSATION EVENT', event)
const account = new Account({
pubkey: pkConversation,
privkey: skConversation
})
let plaintext = await account.decrypt(
this.recipient,
event.content
)
if (plaintext) {
const event2 = new Event(JSON.parse(plaintext))
window.hiPhilipp(event2)
}
})


// The content body of the handshake message is a JSON
// object with three properties {"pubkey":"", "convkey":"", "sig":""}
// with the real pubkey of the user trying to message you,
// the conversation pubkey where the messages are being posted to,
// and a signature for a fake event that looks as follows:
const handshakeEvent = new EventBuilder({
kind: 0,
pubkey: this.app.myPubkey,
content: '',
tags: [['p', pkConversation]]
}).build()
handshakeEvent.created_at = 0 // Override default created_at
if (!(await this.app.signEvent(handshakeEvent))) return
console.log('GENERATED HANDSHAKE', handshakeEvent)

const handshakeMessage = JSON.stringify({
pubkey: this.app.myPubkey,
convkey: pkConversation,
sig: handshakeEvent.sig
})

// The handshake event is signed with a one-time-use pubkey/privkey
// pair
const skHandshake = generatePrivateKey()
const pkHandshake = getPublicKey(skHandshake)
const handshakeAccount = new Account({
pubkey: pkHandshake,
privkey: skHandshake
})
const ciphertext = await handshakeAccount.encrypt(
this.recipient,
`INCOGNITO DIRECT MESSAGE\n\nYour client doesn't support incognito direct messages\n\n---------\n${handshakeMessage}`
)
if (!ciphertext) return

const event = EventBuilder.message(
pkHandshake,
this.recipient,
ciphertext
).build()
if (!(await handshakeAccount.sign(event))) return

console.log('Handshake event:', event)

if (await this.nostr.publish(event)) {
this.reset()
this.$nextTick(this.focus.bind(this))
this.$emit('publish', event)
} else {
this.$q.notify({
message: $t(`Failed to send message`),
color: 'negative',
})
}
} else {
this.$q.notify({
message: $t(`Failed to send message`),
color: 'negative',
pkConversation = getPublicKey(skConversation)
}

// Send our message using the conversation key to a random key
{
// All private messages have an outer "decoy" shell NIP-04
// event with the conversation pubkey as sender and a random
// recipient.
// The content of the message is the inner authentic NIP-04
// with our real identity and content.

// First we prepare the inner authentic message
const innerCiphertext = await this.app.activeAccount.encrypt(
this.recipient,
content
)
const innerEvent = EventBuilder.message(
this.app.myPubkey,
this.recipient,
innerCiphertext
).build()
if (!(await this.app.activeAccount.sign(innerEvent))) return

// Next, we prepare the outer message
const conversationAccount = new Account({
pubkey: pkConversation,
privkey: skConversation
})
const outerCiphertext = await conversationAccount.encrypt(
this.recipient,
JSON.stringify(innerEvent)
)
console.log('Inner event:', innerEvent)

const randomRecipient = generatePrivateKey()
const outerEvent = EventBuilder.message(
pkConversation,
randomRecipient,
outerCiphertext
).build()
if (!(await conversationAccount.sign(outerEvent))) return
console.log('Outer event:', outerEvent)

if (await this.nostr.publish(outerEvent)) {
this.reset()
this.$nextTick(this.focus.bind(this))
this.$emit('publish', outerEvent)
} else {
this.$q.notify({
message: $t(`Failed to send message`),
color: 'negative',
})
}
}

this.publishing = false
Expand All @@ -138,13 +251,15 @@ export default {
align-items: flex-end;
padding: 0 1rem;
width: 100%;

.input-section {
width: 100%;
background-color: rgba($color: $color-dark-gray, $alpha: 0.2);
border-radius: 1rem;
position: relative;
padding: 12px 36px 12px 1rem;
margin-right: 0.5rem;

textarea {
display: block;
width: 100%;
Expand All @@ -158,33 +273,38 @@ export default {
-webkit-appearance: none;
resize: none;
border: none;

&:focus {
border: none;
outline: none;
}
}
}

.inline-controls {
position: absolute;
right: 4px;
bottom: 5px;

&-item {
width: 32px;
height: 32px;
border-radius: 999px;
cursor: pointer;
padding: 5px;

svg {
width: 100%;
fill: $color-primary;
}

&:hover {
background-color: rgba($color: $color-primary, $alpha: 0.3);
}
}
}
.controls {
}

.controls {}
}
</style>
<style lang="scss">
Expand Down
1 change: 1 addition & 0 deletions src/nostr/NostrClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default class NostrClient {
this.pool = new RelayPool(relays)
this.pool.on('notice', this.onNotice.bind(this))
this.pool.on('ok', this.onOk.bind(this))
window.clientSubscribe = this.subscribe.bind(this)
}

connect() {
Expand Down
5 changes: 4 additions & 1 deletion src/nostr/NostrStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ export const useNostrStore = defineStore('nostr', {
},

addEvent(event, relay = null) {
if (!window.hiPhilipp) {
window.hiPhilipp = this.addEvent.bind(this)
}
// console.log(`[EVENT] from ${relay}`, event)

if (relay?.url) {
Expand Down Expand Up @@ -180,7 +183,7 @@ export const useNostrStore = defineStore('nostr', {

// Subscribe to events tagging us
const subTags = this.client.subscribe({
kinds: [EventKind.NOTE, EventKind.REACTION, EventKind.SHARE, EventKind.DM],
kinds: [EventKind.NOTE, EventKind.REACTION, EventKind.SHARE, EventKind.DM, 808],
'#p': [pubkey],
limit: 400,
}, `notifications:${pubkey.substr(0, 40)}`)
Expand Down
Loading