Skip to content

Commit

Permalink
feat: add the publishPrivateMessage API
Browse files Browse the repository at this point in the history
  • Loading branch information
hackergrrl committed Jan 27, 2020
1 parent c2bc603 commit 3c788b9
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 0 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ documented types include
}
```

#### cabal.publishPrivateMessage(text, recipientKey, cb)

Write the private message string `text` to be encrypted so that only
`recipientKey` (the public key of its recipient) can read it.

A `timestamp` field is set automatically with the current local system time.

## License

AGPLv3
34 changes: 34 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var sublevel = require('subleveldown')
var crypto = require('hypercore-crypto')
var url = require('url')
var querystring = require('query-string')
var {box} = require('./lib/crypto')
var createChannelView = require('./views/channels')
var createMembershipsView = require('./views/channel-membership')
var createMessagesView = require('./views/messages')
Expand Down Expand Up @@ -145,6 +146,39 @@ Cabal.prototype.publish = function (message, opts, cb) {
})
}

/**
* Publish a message to your feed, encrypted to specific recipient's key.
* @param {String} text - The textual message to publish.
* @param {String|Buffer[32]) recipientKey - A recipient's public key to encrypt the message to.
* @param {function} cb - When the message has been successfully written.
*/
Cabal.prototype.publishPrivateMessage = function (text, recipientKey, cb) {
if (!cb) cb = noop
if (typeof text !== 'string') return cb(new Error('text must be a string'))
if (!isHypercoreKey(recipientKey)) return cb(new Error('recipientKey must be a 32-byte hypercore key'))

if (typeof recipientKey === 'string') recipientKey = Buffer.from(recipientKey, 'hex')

this.feed(function (feed) {
const message = {
type: 'private/text',
content: {
text
},
timestamp: timestamp()
}
const ciphertext = box(Buffer.from(JSON.stringify(message)), [recipientKey])
const encryptedMessage = {
type: 'encrypted',
content: ciphertext
}

feed.append(encryptedMessage, function (err) {
cb(err, err ? null : encryptedMessage)
})
})
}

Cabal.prototype.publishNick = function (nick, cb) {
// TODO: sanity checks on reasonable names
if (!cb) cb = noop
Expand Down
27 changes: 27 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
var pb = require('private-box')
var sodium = require('sodium-universal')

module.exports = {
box: box,
unbox: unbox
}

// Data buffer, array of hypercore public key buffers
// Returns ciphertext buffer
function box (data, recipients) {
if (!Array.isArray(recipients)) recipients = [recipients]
recipients = recipients.map(function (key) {
var pkbuf = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES)
sodium.crypto_sign_ed25519_pk_to_curve25519(pkbuf, key)
return pkbuf
})
return pb.encrypt(data, recipients)
}

// Encrypted data buffer, hypercore secret key
// Returns decrypted buffer, or undefined if not addressed to the key
function unbox (cdata, key) {
var skbuf = Buffer.alloc(sodium.crypto_box_SECRETKEYBYTES)
sodium.crypto_sign_ed25519_sk_to_curve25519(skbuf, key)
return pb.decrypt(cdata, skbuf)
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@
"materialized-group-auth": "^1.1.1",
"monotonic-timestamp": "0.0.9",
"once": "^1.4.0",
"private-box": "^0.3.1",
"pump": "^3.0.0",
"query-string": "^6.8.2",
"randombytes": "^2.0.6",
"read-only-stream": "^2.0.0",
"readable-stream": "^3.4.0",
"sodium-universal": "^2.0.0",
"strftime": "^0.10.0",
"subleveldown": "^4.1.0",
"through2": "^3.0.1",
Expand Down
47 changes: 47 additions & 0 deletions test/private.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Tests for private messages.

const Cabal = require('..')
const test = require('tape')
const ram = require('random-access-memory')
const crypto = require('hypercore-crypto')
const {unbox} = require('../lib/crypto')

test('write a private message & check it\'s not plaintext', function (t) {
t.plan(4)

const keypair = crypto.keyPair()

const cabal = Cabal(ram)
cabal.ready(function () {
cabal.publishPrivateMessage('greetings', keypair.publicKey, function (err, cipherMsg) {
t.error(err)
t.same(cipherMsg.type, 'encrypted', 'type is "encrypted"')
t.ok(Buffer.isBuffer(cipherMsg.content), 'content is a buffer')
t.notSame(cipherMsg.content.toString(), 'greetings')
})
})
})

test('write a private message & manually decrypt', function (t) {
t.plan(5)

const keypair = crypto.keyPair()

const cabal = Cabal(ram)
cabal.ready(function () {
cabal.publishPrivateMessage('hello', keypair.publicKey, function (err, cipherMsg) {
t.error(err)
t.same(cipherMsg.type, 'encrypted', 'type is "encrypted"')

const plaintext = unbox(cipherMsg.content, keypair.secretKey).toString()
try {
const message = JSON.parse(plaintext)
t.same(message.type, 'private/text', 'type is ok')
t.same(typeof message.content, 'object', 'content is set')
t.same(message.content.text, 'hello', 'text is ok')
} catch (err) {
t.error(err)
}
})
})
})

0 comments on commit 3c788b9

Please sign in to comment.