diff --git a/web/lib/express.js b/web/lib/express.js index f255d22..a898cb1 100644 --- a/web/lib/express.js +++ b/web/lib/express.js @@ -5,13 +5,8 @@ const port = process.env.PORT || 7624; const bodyParser = require("body-parser"); const { asyncRouter, - writeKey, - nextIdentityFilename, reconstructKeys, - readKey, uploadPicture, - identityFilename, - ssbFolder, isPhone, } = require("./utils"); const queries = require("./queries"); @@ -77,18 +72,15 @@ app.use(async (req, res, next) => { }; res.locals.context = req.context; try { - const identities = await ssb.client().identities.list(); const key = req.signedCookies["ssb_key"]; if (!key) return next(); const parsedKey = JSON.parse(key); - if (!identities.includes(parsedKey.id)) { - const filename = await nextIdentityFilename(ssb.client()); + if (!parsedKey.id) return next(); - writeKey(key, `/identities/${filename}`); - ssb.client().identities.refresh(); - } + ssb.client().identities.addUnboxer(parsedKey); req.context.profile = await queries.getProfile(parsedKey.id); + req.context.profile.key = parsedKey; const isRootUser = req.context.profile.id == ssb.client().id || @@ -199,11 +191,11 @@ const doLogin = async (submittedKey, res) => { }; router.get("/login", { public: true }, async (req, res) => { - const login_key = + const loginKey = req.query.key && Buffer.from(req.query.key, "base64").toString("utf8"); - if (login_key) { - await doLogin(JSON.parse(login_key), res); + if (loginKey) { + await doLogin(loginKey, res); } else { res.render("shared/login", { mode }); } @@ -241,31 +233,26 @@ router.post("/signup", { public: true }, async (req, res) => { const pictureLink = picture && (await uploadPicture(ssb.client(), picture)); - const filename = await nextIdentityFilename(ssb.client()); - const profileId = await ssb.client().identities.create(); - const key = readKey(`/identities/${filename}`); - if (key.id != profileId) - throw "profileId and key.id don't match, probably race condition, bailing out for safety"; - - debug("Created new user with id", profileId); - + const key = await ssb.client().identities.createNewKey(); res.cookie("ssb_key", JSON.stringify(key), cookieOptions); - key.private = "[removed]"; - debug("Generated key", key); await ssb.client().identities.publishAs({ - id: profileId, + key, private: false, content: { type: "about", - about: profileId, + about: key.id, name: name, ...(pictureLink ? { image: pictureLink } : {}), }, }); - debug("Published about", { about: profileId, name, image: pictureLink }); - await queries.autofollow(profileId); + const debugKey = { ...key, private: "[removed]" }; + debug("Generated key", debugKey); + + debug("Published about", { about: key.id, name, image: pictureLink }); + + await queries.autofollow(key.id); res.redirect("/keys"); }); @@ -273,7 +260,7 @@ router.post("/signup", { public: true }, async (req, res) => { router.get("/keys", (req, res) => { res.render("shared/keys", { useEmail: process.env.SENDGRID_API_KEY, - key: req.signedCookies["ssb_key"], + key: req.context.profile.key, }); }); @@ -287,8 +274,8 @@ router.post("/keys/email", async (req, res) => { */ const email = req.body.email; const origin = req.body.origin; - const ssb_key = req.signedCookies["ssb_key"]; - const login_key = Buffer.from(JSON.stringify(ssb_key)).toString("base64"); + const ssb_key = JSON.stringify(req.context.profile.key); + const login_key = Buffer.from(ssb_key).toString("base64"); if (process.env.NODE_ENV == "production") { let html = await ejs.renderFile("views/shared/email_sign_in.ejs", { @@ -313,17 +300,33 @@ router.post("/keys/email", async (req, res) => { }); router.get("/keys/copy", (req, res) => { - res.render("shared/keys_copy", { key: req.signedCookies["ssb_key"] }); + res.render("shared/keys_copy", { + key: JSON.stringify(req.context.profile.key), + }); }); router.get("/keys/download", async (req, res) => { - const identities = await ssb.client().identities.list(); - const index = identities.indexOf(req.context.profile.id) - 1; - const filename = identityFilename(index); - const secretPath = `${ssbFolder()}/identities/${filename}`; + const secretFile = ` +# WARNING: Never show this to anyone. +# WARNING: Never edit it or use it on multiple devices at once. +# +# This is your SECRET, it gives you magical powers. With your secret you can +# sign your messages so that your friends can verify that the messages came +# from you. If anyone learns your secret, they can use it to impersonate you. +# +# If you use this secret on more than one device you will create a fork and +# your friends will stop replicating your content. +# +${JSON.stringify(req.context.profile.key)} +# +# The only part of this file that's safe to share is your public name: +# +# ${req.context.profile.id} +`; - res.attachment("secret"); - res.sendFile(secretPath); + res.contentType("text/plain"); + res.header("Content-Disposition", "attachment; filename=secret"); + res.send(secretFile); }); router.get( @@ -367,7 +370,7 @@ router.post("/profile/:id(*)/add_friend", async (req, res) => { } await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "contact", @@ -386,7 +389,7 @@ router.post("/profile/:id(*)/reject_friend", async (req, res) => { } await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "contact", @@ -400,7 +403,7 @@ router.post("/profile/:id(*)/reject_friend", async (req, res) => { router.post("/publish", async (req, res) => { await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "post", @@ -416,7 +419,7 @@ router.post("/publish_secret", async (req, res) => { const recipients = req.body.recipients; await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: true, content: { type: "post", @@ -434,7 +437,7 @@ router.post("/vanish", async (req, res) => { for (const key of keys) { await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "delete", @@ -450,7 +453,7 @@ router.post("/profile/:id(*)/publish", async (req, res) => { const id = req.params.id; await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "post", @@ -466,7 +469,7 @@ router.post("/profile/:id(*)/publish_secret", async (req, res) => { const id = req.params.id; await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: true, content: { type: "post", @@ -519,7 +522,7 @@ router.post("/about", async (req, res) => { if (update.name || update.image || update.description) { await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: update, }); @@ -562,7 +565,7 @@ router.post("/communities/new", async (req, res) => { const post = req.body.post; await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "post", @@ -573,7 +576,7 @@ router.post("/communities/new", async (req, res) => { }); await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "channel", @@ -635,7 +638,7 @@ router.post("/communities/:name/new", async (req, res) => { const post = req.body.post; const topic = await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "post", @@ -652,7 +655,7 @@ router.post("/communities/:name/join", async (req, res) => { const name = req.params.name; await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "channel", @@ -668,7 +671,7 @@ router.post("/communities/:name/leave", async (req, res) => { const name = req.params.name; await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "channel", @@ -686,7 +689,7 @@ router.post("/communities/:name/:key(*)/publish", async (req, res) => { const reply = req.body.reply; await ssb.client().identities.publishAs({ - id: req.context.profile.id, + key: req.context.profile.key, private: false, content: { type: "post", diff --git a/web/lib/monkeypatch/ssb-identities.js b/web/lib/monkeypatch/ssb-identities.js deleted file mode 100644 index f501bc6..0000000 --- a/web/lib/monkeypatch/ssb-identities.js +++ /dev/null @@ -1,139 +0,0 @@ -// 1) Monkeypatched to include the refresh function -// 2) Monkeypatched to allow secret messages without published in the recps -var leftpad = require("left-pad"); -var path = require("path"); -var mkdirp = require("mkdirp"); -var fs = require("fs"); -var ssbKeys = require("ssb-keys"); -var create = require("ssb-validate").create; - -function toTarget(t) { - return "object" === typeof t ? t && t.link : t; -} - -exports.name = "identities"; -exports.version = "1.0.0"; -exports.manifest = { - main: "sync", - list: "async", - create: "async", - publishAs: "async", - help: "sync", - refresh: "sync", -}; - -exports.init = function (sbot, config) { - var dir = path.join(config.path, "identities"); - console.log("identities directory", config.path); - mkdirp.sync(dir); - - function readKeys() { - return fs - .readdirSync(dir) - .filter(function (name) { - return /^secret_\d+\.butt$/.test(name); - }) - .map(function (file) { - return ssbKeys.loadSync(path.join(dir, file)); - }); - } - - var keys = readKeys(); - - var locks = {}; - - sbot.addUnboxer({ - key: function (content) { - for (var i = 0; i < keys.length; i++) { - var key = ssbKeys.unboxKey(content, keys[i]); - if (key) return key; - } - }, - value: function (content, key) { - return ssbKeys.unboxBody(content, key); - }, - }); - - return { - main: function () { - return sbot.id; - }, - refresh: function () { - keys = readKeys(); - }, - list: function (cb) { - cb( - null, - [sbot.id].concat( - keys.map(function (e) { - return e.id; - }) - ) - ); - }, - create: function (cb) { - var filename = "secret_" + leftpad(keys.length, 2, "0") + ".butt"; - ssbKeys.create(path.join(dir, filename), function (err, newKeys) { - keys.push(newKeys); - cb(err, newKeys.id); - }); - }, - publishAs: function (opts, cb) { - var id = opts.id; - if (locks[id]) return cb(new Error("already writing")); - var _keys = - sbot.id === id - ? sbot.keys - : keys.find(function (e) { - return id === e.id; - }); - if (!_keys) return cb(new Error("must provide id of listed identities")); - var content = opts.content; - - var recps = [].concat(content.recps).map(toTarget); - - if (content.recps && !opts.private) - return cb(new Error("recps set, but opts.private not set")); - else if (!content.recps && opts.private) - return cb(new Error("opts.private set, but content.recps not set")); - else if (!!content.recps && opts.private) { - if (!Array.isArray(content.recps) || !content.recps.length) - return cb( - new Error( - "content.recps must be an array containing at least one id, was:" + - JSON.stringify(recps) - ) - ); - content = ssbKeys.box(content, recps); - } - - locks[id] = true; - sbot.getLatest(id, function (err, data) { - var state = data - ? { - id: data.key, - sequence: data.value.sequence, - timestamp: data.value.timestamp, - queue: [], - } - : { id: null, sequence: null, timestamp: null, queue: [] }; - sbot.add( - create( - state, - _keys, - config.caps && config.caps.sign, - content, - Date.now() - ), - function (err, a, b) { - delete locks[id]; - cb(err, a, b); - } - ); - }); - }, - help: function () { - return require("./help"); - }, - }; -}; diff --git a/web/lib/plugins/memory-identities.js b/web/lib/plugins/memory-identities.js new file mode 100644 index 0000000..f772f3f --- /dev/null +++ b/web/lib/plugins/memory-identities.js @@ -0,0 +1,85 @@ +// Differently from ssb-identities, this plugin only keeps keys in memory, as we don't want to save them on the server + +const ssbKeys = require("ssb-keys"); +const { create } = require("ssb-validate"); + +exports.name = "identities"; +exports.version = "1.0.0"; +exports.manifest = { + create: "sync", + addUnboxer: "sync", + publishAs: "async", + createNewKey: "sync", +}; + +let unboxersAdded = []; +let locks = {}; + +const toTarget = (t) => (typeof t == "object" ? t && t.link : t); + +const addUnboxer = (ssb) => (key) => { + if (unboxersAdded.includes(key.id)) return; + + ssb.addUnboxer({ + key: (content) => { + const unboxKey = ssbKeys.unboxKey(content, key); + if (unboxKey) return unboxKey; + }, + value: (content, key) => { + return ssbKeys.unboxBody(content, key); + }, + }); + unboxersAdded.push(key.id); +}; + +const publishAs = (ssb, config) => ({ key, private, content }, cb) => { + const id = key.id; + if (locks[id]) throw new Error("already writing"); + + const recps = [].concat(content.recps).map(toTarget); + + if (content.recps && !private) { + return new Error("recps set, but private not set"); + } else if (!content.recps && private) { + return new Error("private set, but content.recps not set"); + } else if (!!content.recps && private) { + if (!Array.isArray(content.recps) || !~recps.indexOf(id)) + return new Error( + "content.recps must be an array containing publisher id:" + + id + + " was:" + + JSON.stringify(recps) + + " indexOf:" + + recps.indexOf(id) + ); + content = ssbKeys.box(content, recps); + } + + locks[id] = true; + ssb.getLatest(id, (_err, data) => { + const state = data + ? { + id: data.key, + sequence: data.value.sequence, + timestamp: data.value.timestamp, + queue: [], + } + : { id: null, sequence: null, timestamp: null, queue: [] }; + + ssb.add( + create(state, key, config.caps && config.caps.sign, content, Date.now()), + (err, a, b) => { + delete locks[id]; + cb(err, a, b); + } + ); + }); +}; + +exports.init = function (ssb, config) { + return { + addUnboxer: addUnboxer(ssb), + publishAs: publishAs(ssb, config), + createNewKey: ssbKeys.generate, + }; +}; diff --git a/web/lib/ssb.js b/web/lib/ssb.js index 198c3e0..edcf49f 100644 --- a/web/lib/ssb.js +++ b/web/lib/ssb.js @@ -29,7 +29,7 @@ Server.use(require("ssb-master")) .use(require("./monkeypatch/ssb-friends")) .use(require("ssb-query")) .use(require("ssb-device-address")) - .use(require("./monkeypatch/ssb-identities")) + .use(require("./plugins/memory-identities")) .use(require("ssb-peer-invites")) .use(require("ssb-blobs")) .use(require("ssb-private")); diff --git a/web/lib/utils.js b/web/lib/utils.js index 21a80b3..9f8114b 100644 --- a/web/lib/utils.js +++ b/web/lib/utils.js @@ -69,15 +69,6 @@ module.exports.writeKey = (key, path) => { fs.writeFileSync(secretPath, key, { mode: 0x100, flag: "wx" }); }; -module.exports.identityFilename = (index) => { - return "secret_" + leftpad(index, 2, "0") + ".butt"; -}; - -module.exports.nextIdentityFilename = async (ssbClient) => { - const identities = await ssbClient.identities.list(); - return module.exports.identityFilename(identities.length - 1); -}; - // From ssb-keys module.exports.reconstructKeys = (keyfile) => { var privateKey = keyfile @@ -93,13 +84,6 @@ module.exports.reconstructKeys = (keyfile) => { return keys; }; -module.exports.readKey = (path) => { - let secretPath = `${ssbFolder()}${path}`; - - let keyfile = fs.readFileSync(secretPath, "utf8"); - return module.exports.reconstructKeys(keyfile); -}; - module.exports.uploadPicture = async (ssbClient, picture) => { const maxSize = 5 * 1024 * 1024; // 5 MB if (picture.size > maxSize) throw "Max size exceeded"; diff --git a/web/package.json b/web/package.json index c6c64b6..ed3d79f 100644 --- a/web/package.json +++ b/web/package.json @@ -42,7 +42,6 @@ "ssb-device-address": "^1.1.6", "ssb-friends": "^4.1.4", "ssb-gossip": "^1.1.1", - "ssb-identities": "^2.1.1", "ssb-invite": "^2.1.4", "ssb-keys": "^7.2.2", "ssb-master": "^1.0.3", @@ -57,4 +56,4 @@ "devDependencies": { "electron": "^8.2.0" } -} +} \ No newline at end of file