Verified Commit 0dc2f8ac authored by Aral Balkan's avatar Aral Balkan
Browse files

Rename to Secure Ephemeral Messaging Channel

parent 2c0f343b
......@@ -15,11 +15,11 @@ If you update the _schema.proto_, you must run `npm run protobuf` to generate th
## Usage
```js
const { EphemeralMessagingChannel } = require('@hypha/ephemeral-messaging-channel')
const { SecureEphemeralMessagingChannel } = require('@hypha/secure-ephemeral-messaging-channel')
// Create the channel, passing in the global signing secret key.
// (The channel will derive a separate secret key from it to use for symetric encryption.)
const ephemeralMessagingChannel = new EphemeralMessagingChannel(secretKey)
const secureEphemeralMessagingChannel = new SecureEphemeralMessagingChannel(secretKey)
// Create a database (hypercore, hyperdb, or hyperdrive instance)
const db = hyperdb(filename => ram(filename))
......@@ -27,17 +27,17 @@ const db = hyperdb(filename => ram(filename))
//
// Create your event handlers.
//
ephemeralMessagingChannel.on('message', (database, peer, messageObject) => {
secureEphemeralMessagingChannel.on('message', (database, peer, messageObject) => {
// `peer` has sent `payload` of mimetype `contentType` for `database`
})
ephemeralMessagingChannel.on('received-bad-message', (err, database, peer, messageBuffer) => {
secureEphemeralMessagingChannel.on('received-bad-message', (err, database, peer) => {
// there was an error parsing a received message
})
// Add the database to the ephemeral messaging channel.
ephemeralMessagingChannel.addDatabase(db)
secureEphemeralMessagingChannel.addDatabase(db)
// Register the ‘encrypted-ephemeral’ extension in your replication streams.
const webSwarm = swarm(signalhub(discoveryKey, ['https://localhost:444']))
......@@ -46,7 +46,7 @@ webSwarm.on('peer', function (remoteWebStream) {
// Create the local replication stream.
const localReplicationStream = db.replicate({
live: true,
extensions: ['encrypted-ephemeral']
extensions: ['secure-ephemeral']
})
// Start replicating.
......@@ -61,7 +61,7 @@ webSwarm.on('peer', function (remoteWebStream) {
})
// Use the API
datEphemeralExtMsg.hasSupport(database, peerId)
datEphemeralExtMsg.broadcast(database, messageObject)
datEphemeralExtMsg.send(database, peerId, messageObject)
secureEphemeralMessagingChannel.hasSupport(database, peerId)
secureEphemeralMessagingChannel.broadcast(database, messageObject)
secureEphemeralMessagingChannel.send(database, peerId, messageObject)
```
......@@ -10,31 +10,31 @@ var encodings = require('protocol-buffers-encodings')
var varint = encodings.varint
var skip = encodings.skip
var EncryptedEphemeralMessage = exports.EncryptedEphemeralMessage = {
var SecureEphemeralMessage = exports.SecureEphemeralMessage = {
buffer: true,
encodingLength: null,
encode: null,
decode: null
}
defineEncryptedEphemeralMessage()
defineSecureEphemeralMessage()
function defineEncryptedEphemeralMessage () {
function defineSecureEphemeralMessage () {
var enc = [
encodings.bytes
]
EncryptedEphemeralMessage.encodingLength = encodingLength
EncryptedEphemeralMessage.encode = encode
EncryptedEphemeralMessage.decode = decode
SecureEphemeralMessage.encodingLength = encodingLength
SecureEphemeralMessage.encode = encode
SecureEphemeralMessage.decode = decode
function encodingLength (obj) {
var length = 0
if (!defined(obj.nonce)) throw new Error("nonce is required")
var len = enc[0].encodingLength(obj.nonce)
length += 1 + len
if (!defined(obj.encryptedMessage)) throw new Error("encryptedMessage is required")
var len = enc[0].encodingLength(obj.encryptedMessage)
if (!defined(obj.ciphertext)) throw new Error("ciphertext is required")
var len = enc[0].encodingLength(obj.ciphertext)
length += 1 + len
return length
}
......@@ -47,9 +47,9 @@ function defineEncryptedEphemeralMessage () {
buf[offset++] = 10
enc[0].encode(obj.nonce, buf, offset)
offset += enc[0].encode.bytes
if (!defined(obj.encryptedMessage)) throw new Error("encryptedMessage is required")
if (!defined(obj.ciphertext)) throw new Error("ciphertext is required")
buf[offset++] = 18
enc[0].encode(obj.encryptedMessage, buf, offset)
enc[0].encode(obj.ciphertext, buf, offset)
offset += enc[0].encode.bytes
encode.bytes = offset - oldOffset
return buf
......@@ -62,7 +62,7 @@ function defineEncryptedEphemeralMessage () {
var oldOffset = offset
var obj = {
nonce: null,
encryptedMessage: null
ciphertext: null
}
var found0 = false
var found1 = false
......@@ -82,7 +82,7 @@ function defineEncryptedEphemeralMessage () {
found0 = true
break
case 2:
obj.encryptedMessage = enc[0].decode(buf, offset)
obj.ciphertext = enc[0].decode(buf, offset)
offset += enc[0].decode.bytes
found1 = true
break
......
......@@ -5,7 +5,7 @@ const sodium = require('sodium-universal')
// exported api
// =
class EphemeralMessagingChannel extends EventEmitter {
class SecureEphemeralMessagingChannel extends EventEmitter {
constructor (secretKey) {
super()
this.secretKey = secretKey
......@@ -39,7 +39,7 @@ class EphemeralMessagingChannel extends EventEmitter {
if (watcher) {
var peer = watcher.getPeer(remoteId)
if (peer) {
return remoteSupports(peer, 'encrypted-ephemeral')
return remoteSupports(peer, 'secure-ephemeral')
}
}
return false
......@@ -61,7 +61,7 @@ class EphemeralMessagingChannel extends EventEmitter {
}
}
}
exports.EphemeralMessagingChannel = EphemeralMessagingChannel
exports.SecureEphemeralMessagingChannel = SecureEphemeralMessagingChannel
// internal
// =
......@@ -80,13 +80,13 @@ class DatabaseWatcher {
send (remoteId, message = {}) {
// get peer and assure support exists
var peer = this.getPeer(remoteId)
if (!remoteSupports(peer, 'encrypted-ephemeral')) {
if (!remoteSupports(peer, 'secure-ephemeral')) {
return
}
// send
message = serialize(message, this.secretKey)
getPeerFeedStream(peer).extension('encrypted-ephemeral', message)
getPeerFeedStream(peer).extension('secure-ephemeral', message)
}
broadcast (message) {
......@@ -127,21 +127,21 @@ class DatabaseWatcher {
onPeerAdd (peer) {
getPeerFeedStream(peer).on('extension', (type, codedMessage) => {
// handle ephemeral messages only
if (type !== 'encrypted-ephemeral') return
if (type !== 'secure-ephemeral') return
try {
// Decode the message using protocol buffers.
const decodedMessage = encodings.EncryptedEphemeralMessage.decode(codedMessage)
const decodedMessage = encodings.SecureEphemeralMessage.decode(codedMessage)
const nonce = decodedMessage.nonce
const encryptedMessage = decodedMessage.encryptedMessage
const ciphertext = decodedMessage.ciphertext
if (nonce.length !== sodium.crypto_secretbox_NONCEBYTES) {
throw new Error('Incorrect nonce length.')
}
// Decrypt the message using the secret key.
const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES)
sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, this.secretKey)
const message = Buffer.alloc(ciphertext.length - sodium.crypto_secretbox_MACBYTES)
sodium.crypto_secretbox_open_easy(message, ciphertext, nonce, this.secretKey)
// We expect JSON, parse it.
const messageInUtf8 = message.toString('utf-8')
......@@ -164,6 +164,8 @@ class DatabaseWatcher {
// emit
this.emitter.emit('message', this.database, peer, parsedMessage)
} catch (e) {
// TODO: Improve this: we should return different errors based on specifics
// e.g., decryption failed, etc.
this.emitter.emit('received-bad-message', e, this.database, peer/*, message*/)
}
})
......@@ -181,19 +183,19 @@ function serialize (message, secretKey) {
}
const serialisedMessage = Buffer.from(JSON.stringify(message), 'utf-8')
const encryptedMessage = Buffer.alloc(serialisedMessage.length + sodium.crypto_secretbox_MACBYTES)
const ciphertext = Buffer.alloc(serialisedMessage.length + sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
sodium.randombytes_buf(nonce)
sodium.crypto_secretbox_easy(encryptedMessage, serialisedMessage, nonce, secretKey)
sodium.crypto_secretbox_easy(ciphertext, serialisedMessage, nonce, secretKey)
const messageToEncode = {
nonce,
encryptedMessage
ciphertext
}
// Encode using protocol buffers.
return encodings.EncryptedEphemeralMessage.encode(messageToEncode)
return encodings.SecureEphemeralMessage.encode(messageToEncode)
}
function getPeerFeedStream (peer) {
......
message EncryptedEphemeralMessage {
message SecureEphemeralMessage {
required bytes nonce = 1;
required bytes encryptedMessage = 2;
}
\ No newline at end of file
required bytes ciphertext = 2;
}
# Encrypted Ephemeral Extension Message Specification
# Secure Ephemeral Message Channel Specification
# Summary
[summary]: #summary
This spec defines the non-standard `encrypted-ephemeral` extension message used in the Dat replication protocol. This message provides a way to send arbitrary application data to a peer through an existing connection.
This spec defines the non-standard `secure-ephemeral` extension message used in the Dat replication protocol. This message provides a way to send arbitrary application data to a peer through an existing connection.
# Motivation
......@@ -14,38 +14,35 @@ While Dat is effective at sharing persistent datasets, applications frequently n
This spec is based on the [Dep-0000 Ephemeral Message (Extension Message) spec](https://github.com/beakerbrowser/dat-ephemeral-ext-msg/blob/master/spec.md) which was motivated by the need for a quick solution to these use-cases. That spec establishes a mechanism for sending ephemeral messages over existing Dat connections. This spec diverges from Dep-0000 in three major ways:
1. It narrows the scope of recipients to nodes owned by the same person.
2. It reduces the schema to a single, symmetrically-encrypted field.
3. Limits the structure of the unencrypted message to JSON.
2. It reduces the schema to a nonce and an authenticated symmetrically-encrypted field.
3. Limits the plaintext message format to JSON.
# Reference Documentation
[reference-documentation]: #reference-documentation
This spec is implemented using the Dat replication protocol's "extension messages." In order to broadcast support for this spec, a client should declare the `'encrypted-ephemeral'` extension in the replication handshake.
This spec is implemented using the Dat replication protocol's "extension messages." In order to broadcast support for this spec, a client should declare the `'secure-ephemeral'` extension in the replication handshake.
Encrypted-ephemeral messages can be sent at any time after the connection is established by sending an extension message of type `'encrypted-ephemeral'`. The message payload is a protobuf with the following schema:
Secure-ephemeral messages can be sent at any time after the connection is established by sending an extension message of type `'secure-ephemeral'`. The message payload is a protocol buffer with the following schema:
```
message EncryptedEphemeralMessage {
message SecureEphemeralMessage {
required bytes nonce = 1;
required bytes encryptedMessage = 2;
required bytes ciphertext = 2;
}
```
There is no dictated structure for the unencrypted message.
There is no dictated structure for the plaintext message.
The message is encrypted using `secretbox_easy` from _sodium-universal_ package. That function currently uses the XSalsa20 stream cipher for encryption and a Poly1305 MAC for authentication.
The client may respond to the message by emitting an event, so that it may be handled by the client's application logic. No acknowledgment of receipt will be provided (no "ACK").
The message is encrypted using the `secretbox_easy` function from the _sodium-universal_ package. This currently uses the XSalsa20 stream cipher for encryption and a Poly1305 MAC for authentication.
The client may respond to the message by emitting an event, so that it may be handled by the client's application logic. No acknowledgment of receipt is automatically provided (no "ACK").
# Privacy, security, and reliability
[privacy-security-and-reliability]: #privacy-security-and-reliability
The Dat messaging channel is encrypted using the public key of the first hypercore to be exchanged over the channel. As a result, all traffic can be decrypted and/or modified by an intermediary which possesses the public key. Encrypted ephemeral messages are further authenticated and encrypted with a secret key derived from the secret key of the first hypercore.
Encrypted ephemeral messages thus can only be decrypted by holders of the secret key of the original hypercore.
The Dat messaging channel is encrypted using the public key of the first hypercore to be exchanged over the channel. As a result, all traffic can be decrypted and/or modified by an intermediary which possesses the public key. Secure ephemeral messages are further authenticated and encrypted with a secret key known to the owner of the original hypercore.
Applications should also not assume connectivity will occur between all peers that have "joined the swarm" for a hypercore. There are many factors which may cause a peer not to connect: failed NAT traversal, the client running out of available sockets, or even the intentional blocking of a peer by the discovery network.
Applications should not assume connectivity will occur between all peers that have "joined the swarm" for a hypercore. There are many factors which may cause a peer not to connect: failed NAT traversal, the client running out of available sockets, or even the intentional blocking of a peer by the discovery network.
# Drawbacks
......@@ -56,7 +53,7 @@ Applications should also not assume connectivity will occur between all peers th
# Changelog
[changelog]: #changelog
- 2019-02-10: Update to reflect changes in encrypted ephemeral fork
- 2019-02-10: Update to reflect changes in secure ephemeral message channel fork. Previous entries are for Dep-0000
- 2018-07-02: Add "Privacy, security, and reliability" section
- 2018-06-20: Add a size-limit suggestion
- 2018-06-10: Change the payload encoding to protobuf and provide a more flexible content-type field.
......
......@@ -3,7 +3,7 @@
var tape = require('tape')
var ram = require('random-access-memory')
var { EphemeralMessagingChannel } = require('../../')
var { SecureEphemeralMessagingChannel } = require('../../')
module.exports = function (database) {
......@@ -16,11 +16,11 @@ module.exports = function (database) {
const secretKey = Buffer.from('2e4d36dc3c49049b450a3656188692a328df4b9cff11b6d95157fc21363a1b28', 'hex')
tape(`exchange ephemeral messages: ${databaseName}`, function (t) {
tape(`exchange secure ephemeral messages: ${databaseName}`, function (t) {
// must use 2 instances to represent 2 different nodes
var srcEphemeral = new EphemeralMessagingChannel(secretKey)
var cloneEphemeral = new EphemeralMessagingChannel(secretKey)
var srcEphemeral = new SecureEphemeralMessagingChannel(secretKey)
var cloneEphemeral = new SecureEphemeralMessagingChannel(secretKey)
var src = database(ram)
var clone
......@@ -30,7 +30,7 @@ module.exports = function (database) {
// Isomorphic interface to support hypercore, hyperdb, and hyperdrive.
// The three packages have slightly different APIs that makes this necessary.
// TODO: open issue for unifying the interfaces.
// TODO: open issue at general Dat issue tracker for unifying the interfaces.
var srcFeed = src.source || src.metadata || src
var putFunction = isHyperdrive ? 'writeFile' : isHyperDB ? 'put' : 'append'
......@@ -94,12 +94,12 @@ module.exports = function (database) {
var stream1 = clone.replicate({
id: Buffer.from('clone-stream'),
live: true,
extensions: ['encrypted-ephemeral']
extensions: ['secure-ephemeral']
})
var stream2 = src.replicate({
id: Buffer.from('src-stream'),
live: true,
extensions: ['encrypted-ephemeral']
extensions: ['secure-ephemeral']
})
stream1.pipe(stream2).pipe(stream1)
......@@ -161,7 +161,7 @@ module.exports = function (database) {
})
tape(`no peers causes no issue: ${databaseName}`, function (t) {
var ephemeral = new EphemeralMessagingChannel(secretKey)
var ephemeral = new SecureEphemeralMessagingChannel(secretKey)
var src = database(ram)
src.on('ready', function () {
......@@ -174,8 +174,8 @@ module.exports = function (database) {
tape(`fires received-bad-message: ${databaseName}`, function (t) {
// must use 2 instances to represent 2 different nodes
var srcEphemeral = new EphemeralMessagingChannel(secretKey)
var cloneEphemeral = new EphemeralMessagingChannel(secretKey)
var srcEphemeral = new SecureEphemeralMessagingChannel(secretKey)
var cloneEphemeral = new SecureEphemeralMessagingChannel(secretKey)
var src = database(ram)
var srcFeed = src.source || src.metadata || src
......@@ -203,12 +203,12 @@ module.exports = function (database) {
var stream1 = clone.replicate({
id: Buffer.from('clone-stream'),
live: true,
extensions: ['encrypted-ephemeral']
extensions: ['secure-ephemeral']
})
var stream2 = src.replicate({
id: Buffer.from('src-stream'),
live: true,
extensions: ['encrypted-ephemeral']
extensions: ['secure-ephemeral']
})
stream1.pipe(stream2).pipe(stream1)
......@@ -227,7 +227,7 @@ module.exports = function (database) {
t.ok(cloneEphemeral.hasSupport(clone, cloneFeed.peers[0]), 'clone has support')
// send bad message
srcFeed.peers[0].stream.extension('encrypted-ephemeral', Buffer.from([0,1,2,3]))
srcFeed.peers[0].stream.extension('secure-ephemeral', Buffer.from([0,1,2,3]))
})
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment