Completely remove saving ssb keys to the filesystem, now the key is used on every request from the cookies and stays just in memory

This commit is contained in:
Rogerio Chaves 2020-04-30 18:54:41 +02:00
parent 37ff8c895e
commit 4e430255b6
No known key found for this signature in database
GPG Key ID: E6AF5440509B1D94
6 changed files with 142 additions and 210 deletions

View File

@ -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",

View File

@ -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");
},
};
};

View File

@ -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,
};
};

View File

@ -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"));

View File

@ -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";

View File

@ -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"
}
}
}