diff --git a/README.md b/README.md
index 32f52de..46589ca 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ npm install
### Start the app in development mode
```bash
-quasar dev
+npm run dev
```
@@ -33,6 +33,6 @@ npm run format
### Build the app for production
```bash
-quasar build
+npm run build
```
diff --git a/package.json b/package.json
index 9a916f8..31788db 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/components/Message/EncryptedMessage.vue b/src/components/Message/EncryptedMessage.vue
index aaf6f13..0457075 100644
--- a/src/components/Message/EncryptedMessage.vue
+++ b/src/components/Message/EncryptedMessage.vue
@@ -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',
@@ -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)
diff --git a/src/components/Message/MessageEditor.vue b/src/components/Message/MessageEditor.vue
index 6d62e0c..4893789 100644
--- a/src/components/Message/MessageEditor.vue
+++ b/src/components/Message/MessageEditor.vue
@@ -2,14 +2,8 @@
+v-model="content" ref="textarea" :placeholder="placeholder" :disabled="publishing" :rows="1"
+ @submit="publishMessage" submit-on-enter />
@@ -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',
@@ -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
@@ -138,6 +251,7 @@ 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);
@@ -145,6 +259,7 @@ export default {
position: relative;
padding: 12px 36px 12px 1rem;
margin-right: 0.5rem;
+
textarea {
display: block;
width: 100%;
@@ -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 {}
}