aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorcel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519>2017-05-25 16:06:28 -1000
committercel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519>2017-05-26 13:40:38 -1000
commit62e7e74bd278473cc4358700b7f2b5c0a78ac681 (patch)
tree8519cd1fbf0bb3827922f59646d28fe6258768df /lib
parent6dbfedff2f7246430f4e6da100bc3baed0ef4ce1 (diff)
downloadpatchfoo-62e7e74bd278473cc4358700b7f2b5c0a78ac681.tar.gz
patchfoo-62e7e74bd278473cc4358700b7f2b5c0a78ac681.zip
Encrypt blobs in private messages
Diffstat (limited to 'lib')
-rw-r--r--lib/app.js56
-rw-r--r--lib/render.js11
-rw-r--r--lib/serve.js71
3 files changed, 104 insertions, 34 deletions
diff --git a/lib/app.js b/lib/app.js
index 4219151..3a41086 100644
--- a/lib/app.js
+++ b/lib/app.js
@@ -11,6 +11,10 @@ var Contacts = require('ssb-contact')
var About = require('./about')
var Serve = require('./serve')
var Render = require('./render')
+var BoxStream = require('pull-box-stream')
+var crypto = require('crypto')
+
+var zeros = new Buffer(24); zeros.fill(0)
module.exports = App
@@ -168,16 +172,54 @@ App.prototype.publish = function (content, cb) {
tryPublish(2)
}
-App.prototype.addBlob = function (cb) {
+App.prototype.addBlobRaw = function (cb) {
var done = multicb({pluck: 1, spread: true})
- var hashCb = done()
- var addCb = done()
- done(function (err, hash, add) {
- cb(err, hash)
+ var sink = pull(
+ hasher(done()),
+ u.pullLength(done()),
+ this.sbot.blobs.add(done())
+ )
+ done(function (err, hash, size, _) {
+ if (err) return cb(err)
+ cb(null, {link: hash, size: size})
})
+ return sink
+}
+
+App.prototype.addBlob = function (isPrivate, cb) {
+ if (!isPrivate) return this.addBlobRaw(cb)
+ else return this.addBlobPrivate(cb)
+}
+
+App.prototype.addBlobPrivate = function (cb) {
+ var bufs = []
+ var self = this
+ // use the hash of the cleartext as the key to encrypt the blob
+ var hash = crypto.createHash('sha256')
+ return pull.drain(function (buf) {
+ bufs.push(buf)
+ hash.update(buf)
+ }, function (err) {
+ if (err) return cb(err)
+ var secret = hash.digest()
+ pull(
+ pull.values(bufs),
+ BoxStream.createBoxStream(secret, zeros),
+ self.addBlobRaw(function (err, link) {
+ if (err) return cb(err)
+ link.key = secret.toString('base64')
+ cb(null, link)
+ })
+ )
+ })
+}
+
+App.prototype.getBlob = function (id, key) {
+ if (!key) return this.sbot.blobs.get(id)
+ if (typeof key === 'string') key = new Buffer(key, 'base64')
return pull(
- hasher(hashCb),
- this.sbot.blobs.add(addCb)
+ this.sbot.blobs.get(id),
+ BoxStream.createUnboxStream(key, zeros)
)
}
diff --git a/lib/render.js b/lib/render.js
index dd6d55e..3b4a142 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -129,6 +129,7 @@ Render.prototype.markdown = function (text, mentions) {
Render.prototype.imageUrl = function (ref) {
var m = /^blobstore:(.*)/.exec(ref)
if (m) ref = m[1]
+ ref = ref.replace(/#/, '%23')
return this.opts.img_base + ref
}
@@ -161,8 +162,14 @@ Render.prototype.toUrl = function (href) {
if (!u.isRef(href)) return false
return this.opts.base + href
case '&':
- if (!u.isRef(href)) return false
- return this.opts.blob_base + href
+ var parts = href.split('#')
+ var hash = parts.shift()
+ var key = parts.shift()
+ var fragment = parts.join('#')
+ if (!u.isRef(hash)) return false
+ return this.opts.blob_base + hash
+ + (key ? encodeURIComponent('#' + key) : '')
+ + (fragment ? '#' + fragment : '')
case '#': return this.opts.base + 'channel/' +
encodeURIComponent(href.substr(1))
case '/': return this.opts.base + href.substr(1)
diff --git a/lib/serve.js b/lib/serve.js
index 8d3eeba..1ce18c6 100644
--- a/lib/serve.js
+++ b/lib/serve.js
@@ -24,7 +24,7 @@ module.exports = Serve
var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs')
-var urlIdRegex = /^(?:\/+(([%&@]|%25)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))(?:\.([^?]*))?|(\/.*?))(?:\?(.*))?$/
+var urlIdRegex = /^(?:\/+(([%&@]|%25)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3D)\.(?:sha256|ed25519))([^?]*)?|(\/.*?))(?:\?(.*))?$/
function ctype(name) {
switch (name && /[^.\/]*$/.exec(name)[0] || 'html') {
@@ -75,19 +75,18 @@ Serve.prototype.go = function () {
gotData(err, data)
})
busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
- var done = multicb({pluck: 1, spread: true})
var cb = filesCb()
pull(
toPull(file),
- u.pullLength(done()),
- self.app.addBlob(done())
+ self.app.addBlob(!!data.private, function (err, link) {
+ if (err) return cb(err)
+ if (link.size === 0 && !filename) return cb()
+ link.name = filename
+ link.type = mimetype
+ data[fieldname] = link
+ cb()
+ })
)
- done(function (err, size, id) {
- if (err) return cb(err)
- if (size === 0 && !filename) return cb()
- data[fieldname] = {link: id, name: filename, type: mimetype, size: size}
- cb()
- })
})
busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
if (!(fieldname in data)) data[fieldname] = val
@@ -197,7 +196,7 @@ Serve.prototype.handle = function () {
case '%25': m[2] = '%'; m[1] = decodeURIComponent(m[1])
case '%': return this.id(m[1], m[3])
case '@': return this.userFeed(m[1], m[3])
- case '&': return this.blob(m[1])
+ case '&': return this.blob(m[1], m[3])
default: return this.path(m[4])
}
}
@@ -809,7 +808,7 @@ function threadHeads(msgs, rootId) {
}
-Serve.prototype.id = function (id, ext) {
+Serve.prototype.id = function (id, path) {
var self = this
if (self.query.raw != null) return self.rawId(id)
@@ -845,16 +844,14 @@ Serve.prototype.id = function (id, ext) {
channel: channel,
}),
self.wrapPage(id),
- self.respondSink(200, {
- 'Content-Type': ctype(ext),
- })
+ self.respondSink(200)
)
})
)
})
}
-Serve.prototype.userFeed = function (id, ext) {
+Serve.prototype.userFeed = function (id, path) {
var self = this
var q = self.query
var opts = {
@@ -874,9 +871,7 @@ Serve.prototype.userFeed = function (id, ext) {
self.wrapMessages(),
self.wrapUserFeed(isScrolled, id),
self.wrapPage(about.name || id),
- self.respondSink(200, {
- 'Content-Type': ctype(ext)
- })
+ self.respondSink(200)
)
})
}
@@ -905,10 +900,27 @@ Serve.prototype.emoji = function (emoji) {
serveEmoji(this.req, this.res, emoji)
}
-Serve.prototype.blob = function (id) {
+Serve.prototype.blob = function (id, path) {
var self = this
var blobs = self.app.sbot.blobs
- if (self.req.headers['if-none-match'] === id) return self.respond(304)
+ var etag = id + (path || '')
+ if (self.req.headers['if-none-match'] === etag) return self.respond(304)
+ var key
+ if (path) {
+ path = decodeURIComponent(path)
+ if (path[0] === '#') {
+ try {
+ key = new Buffer(path.substr(1), 'base64')
+ } catch(err) {
+ return self.respond(400, err.message)
+ }
+ if (key.length !== 32) {
+ return self.respond(400, 'Bad blob key')
+ }
+ } else {
+ return self.respond(400, 'Bad blob request')
+ }
+ }
var done = multicb({pluck: 1, spread: true})
blobs.want(id, function (err, has) {
if (err) {
@@ -918,7 +930,7 @@ Serve.prototype.blob = function (id) {
if (!has) return self.respond(404, 'Not found')
blobs.size(id, done())
pull(
- blobs.get(id),
+ self.app.getBlob(id, key),
pull.map(Buffer),
ident(done().bind(self, null)),
self.respondSink()
@@ -927,11 +939,14 @@ Serve.prototype.blob = function (id) {
if (err) console.trace(err)
type = type && mime.lookup(type)
if (type) self.res.setHeader('Content-Type', type)
- if (typeof size === 'number') self.res.setHeader('Content-Length', size)
+ // don't serve size for encrypted blob, because it refers to the size of
+ // the ciphertext
+ if (typeof size === 'number' && !key)
+ self.res.setHeader('Content-Length', size)
if (self.query.name) self.res.setHeader('Content-Disposition',
'inline; filename='+encodeDispositionFilename(self.query.name))
self.res.setHeader('Cache-Control', 'public, max-age=315360000')
- self.res.setHeader('etag', id)
+ self.res.setHeader('etag', etag)
self.res.writeHead(200)
})
})
@@ -1290,6 +1305,7 @@ Serve.prototype.wrapThread = function (opts) {
branches: opts.branches,
postBranches: opts.postBranches,
recps: recps,
+ private: opts.recps != null,
}, function (err, composer) {
if (err) return cb(err)
cb(null, [
@@ -1428,6 +1444,7 @@ Serve.prototype.composer = function (opts, cb) {
blobs[data.upload.link] = {
type: data.upload.type,
size: data.upload.size,
+ key: data.upload.key,
}
}
if (data.blob_type && blobs[data.blob_link]) {
@@ -1443,11 +1460,13 @@ Serve.prototype.composer = function (opts, cb) {
}
if (data.upload) {
+ var href = data.upload.link
+ + (data.upload.key ? '#' + data.upload.key : '')
// TODO: be able to change the content-type
var isImage = /^image\//.test(data.upload.type)
data.text = (data.text ? data.text + '\n' : '')
+ (isImage ? '!' : '')
- + '[' + data.upload.name + '](' + data.upload.link + ')'
+ + '[' + data.upload.name + '](' + href + ')'
}
// get bare feed names
@@ -1509,6 +1528,8 @@ Serve.prototype.composer = function (opts, cb) {
h('table.ssb-msgs',
h('tr.msg-row',
h('td.msg-left', {colspan: 2},
+ opts.private ?
+ h('input', {type: 'hidden', name: 'private', value: '1'}) : '',
h('input', {type: 'file', name: 'upload'})
),
h('td.msg-right',