From 62e7e74bd278473cc4358700b7f2b5c0a78ac681 Mon Sep 17 00:00:00 2001 From: cel Date: Thu, 25 May 2017 16:06:28 -1000 Subject: Encrypt blobs in private messages --- lib/serve.js | 71 +++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 25 deletions(-) (limited to 'lib/serve.js') 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', -- cgit v1.2.3