diff options
author | cel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519> | 2017-09-29 08:41:55 -1000 |
---|---|---|
committer | cel <cel@f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519> | 2017-09-29 08:41:55 -1000 |
commit | 440274d19f302e0f293d56cba685e39e372cda12 (patch) | |
tree | 5bcd754ab04f5080aaf353dfc2b60b5fac7edb31 | |
parent | 7fffac329b2718f0ea918cd9fd53d3e2b47cc93e (diff) | |
parent | 62e7e74bd278473cc4358700b7f2b5c0a78ac681 (diff) | |
download | patchfoo-440274d19f302e0f293d56cba685e39e372cda12.tar.gz patchfoo-440274d19f302e0f293d56cba685e39e372cda12.zip |
Merge branch 'secretblobs' into master
-rw-r--r-- | lib/app.js | 54 | ||||
-rw-r--r-- | lib/render.js | 13 | ||||
-rw-r--r-- | lib/serve.js | 97 | ||||
-rw-r--r-- | package.json | 1 |
4 files changed, 129 insertions, 36 deletions
@@ -14,6 +14,10 @@ var Git = require('./git') var cat = require('pull-cat') var proc = require('child_process') var toPull = require('stream-to-pull-stream') +var BoxStream = require('pull-box-stream') +var crypto = require('crypto') + +var zeros = new Buffer(24); zeros.fill(0) module.exports = App @@ -198,8 +202,54 @@ App.prototype.wantSizeBlob = function (id, cb) { }) } -App.prototype.addBlob = function (cb) { - return this.sbot.blobs.add(cb) +App.prototype.addBlobRaw = function (cb) { + var done = multicb({pluck: 1, spread: true}) + var sink = pull( + u.pullLength(done()), + this.sbot.blobs.add(done()) + ) + done(function (err, size, hash) { + 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( + this.sbot.blobs.get(id), + BoxStream.createUnboxStream(key, zeros) + ) } App.prototype.pushBlob = function (id, cb) { diff --git a/lib/render.js b/lib/render.js index d7fe04f..cc08259 100644 --- a/lib/render.js +++ b/lib/render.js @@ -136,7 +136,7 @@ Render.prototype.markdown = function (text, mentions) { else if (link.host === 'http://localhost:7777') mentionsObj[link.href] = link.link if (link.link) - mentionsByLink[link.link] = link + mentionsByLink[link.link + (link.key ? '#' + link.key : '')] = link }) var out = marked(String(text), this.markedOpts) delete this._mentions @@ -147,6 +147,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 + 'image/' + ref } @@ -196,8 +197,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 30689eb..839725a 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -27,7 +27,7 @@ module.exports = Serve var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs') var hlCssDir = path.join(require.resolve('highlight.js'), '../../styles') -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') { @@ -83,20 +83,18 @@ Serve.prototype.go = function () { else data[name] = [data[name], value] } 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 + addField(fieldname, link) + cb() + }) ) - done(function (err, size, id) { - if (err) return cb(err) - if (size === 0 && !filename) return cb() - addField(fieldname, - {link: id, name: filename, type: mimetype, size: size}) - cb() - }) }) busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) { addField(fieldname, val) @@ -236,7 +234,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]) } } @@ -970,7 +968,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) @@ -1006,16 +1004,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 = { @@ -1035,9 +1031,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) ) }) } @@ -1070,17 +1064,33 @@ Serve.prototype.highlight = function (dirs) { this.file(path.join(hlCssDir, dirs)) } -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') + } + } self.app.wantSizeBlob(id, function (err, size) { if (err) { if (/^invalid/.test(err.message)) return self.respond(400, err.message) else return self.respond(500, err.message || err) } pull( - blobs.get(id), + self.app.getBlob(id, key), pull.map(Buffer), ident(gotType), self.respondSink() @@ -1088,22 +1098,41 @@ Serve.prototype.blob = function (id) { function gotType(type) { 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) } }) } Serve.prototype.image = function (path) { - var id = path.substr(1) - var etag = 'image-' + id var self = this - var blobs = self.app.sbot.blobs + var id, key + var m = urlIdRegex.exec(path) + if (m && m[2] === '&') id = m[1], path = m[3] + var etag = 'image-' + id + (path || '') if (self.req.headers['if-none-match'] === etag) return self.respond(304) + 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') + } + } self.app.wantSizeBlob(id, function (err, size) { if (err) { if (/^invalid/.test(err.message)) return self.respond(400, err.message) @@ -1115,7 +1144,7 @@ Serve.prototype.image = function (path) { var heresTheType = done().bind(self, null) pull( - blobs.get(id), + self.app.getBlob(id, key), pull.map(Buffer), ident(heresTheType), pull.collect(onFullBuffer) @@ -2076,6 +2105,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, [ @@ -2215,6 +2245,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]) { @@ -2243,11 +2274,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 @@ -2342,6 +2375,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', diff --git a/package.json b/package.json index eea7838..7f10c07 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "looper": "^4.0.0", "mime-types": "^2.1.12", "multicb": "^1.2.1", + "pull-box-stream": "^1.0.12", "pull-cat": "^1.1.11", "pull-git-packidx-parser": "^1.0.0", "pull-hyperscript": "^0.2.2", |