Skip to content
This repository has been archived by the owner on Jan 25, 2021. It is now read-only.

PGP signing support #11

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ yarn.lock
tunnel.sh

uploads/*
images/*
images/*
keys/*
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"debug": "babel-node src/app.ts --extensions \".ts\" --inspect-brk=9889",
"build:dev": "babel-node src/app.ts --extensions \".ts\"",
"build:prod": "babel src -d dist/ --extensions \".ts\" && webpack --env.production --config ./src/config/webpack.prod",
"ts-check": "tsc"
"ts-check": "tsc",
"keygen": "node src/utils/pgpKeygen.js"
},
"lint-staged": {
"src/**/**.{ts,tsx}": [
Expand Down Expand Up @@ -75,6 +76,7 @@
"node-sass": "^4.7.2",
"node-stream-zip": "^1.8.0",
"nodemon": "^1.17.2",
"openpgp": "^4.4.10",
"postcss-flexbugs-fixes": "3.2.0",
"postcss-loader": "^2.1.3",
"prettier": "^1.16.4",
Expand Down
7 changes: 6 additions & 1 deletion src/server/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ router.post("/signOut", catchErrors(authSessionManager.signOff()));
router.post("/register", catchErrors(authSessionManager.register()));
router.use(
checkAuthorization().unless({
path: [{ url: "/api/v1/mod", methods: ["GET"] }, { url: "/api/v1/user", methods: ["GET"] }, "/api/v1/user/current", "/api/v1/user/create", "/api/v1/version"]
path: [
{ url: "/api/v1/mod", methods: ["GET"] },
{ url: "/api/v1/version", methods: ["GET"] },
"/api/v1/user/current",
"/api/v1/user/create"
]
})
);
router.use("/mod", modRouter);
Expand Down
8 changes: 7 additions & 1 deletion src/server/v1/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { catchErrors } from "../modules/Utils";
import ModService from "./modules/ModService";

import multer from "multer";

const storage = multer.memoryStorage();
const upload = multer({ limits: { fileSize: 75 * 1024 * 1024 }, storage });

Expand All @@ -13,7 +14,12 @@ router.get(
"/",
catchErrors(async (req, res, next) => {
const modService = new ModService(req.ctx);
return res.send(await modService.list(req.query));
let pgp = false;
if (req.query.hasOwnProperty("pgp")) {
res.set("Content-Type", "text/plain");
pgp = true;
}
return res.send(await modService.list(req.query, pgp));
})
);

Expand Down
68 changes: 66 additions & 2 deletions src/server/v1/modules/ModService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import path from "path";
const StreamZip = require("node-stream-zip");
import AuditLogService from "./AuditLogService";
import DiscordManager from "../../modules/DiscordManager";
const openpgp = require("openpgp");
let privkey;
// @ts-ignore
import { gameVersions } from "../../../config/lists";
export default class ModService {
Expand Down Expand Up @@ -50,7 +52,7 @@ export default class ModService {
$options: "i"
};
}
public async list(params?: any) {
public async list(params?: any, pgp: boolean = false) {
const query: dynamic = {};
let sort: dynamic | undefined;
if (params && Object.keys(params).length) {
Expand Down Expand Up @@ -93,7 +95,27 @@ export default class ModService {
}
}
}
return mods.map(mod => mod as IDbMod).map(mod => (mod.gameVersion ? mod : { ...mod, gameVersion: gameVersions[gameVersions.length - 1] }));

const modMap = mods.map(mod => mod as IDbMod).map(mod => (mod.gameVersion ? mod : { ...mod, gameVersion: gameVersions[gameVersions.length - 1] }));
if (!pgp) {
return modMap;
} else {
if (privkey === undefined) {
privkey = await readPrivkey();
}
return await new Promise((res, rej) => {
const signOptions = {
message: openpgp.cleartext.fromText(JSON.stringify(modMap, null, 2)),
privateKeys: [privkey]
};
openpgp
.sign(signOptions)
.then(signed => {
res(signed.data);
})
.catch(err => rej(err));
});
}
}

public async update(changes: IDbMod, isInsert = false) {
Expand Down Expand Up @@ -210,13 +232,22 @@ export default class ModService {
};
const { _id } = (await this.insert(mod)) as IDbMod & { _id: Id };
mod._id = toId(_id);
if (privkey === undefined) {
try {
privkey = await readPrivkey();
} catch (err) {
console.error("ModService.create", "KEY Read", err);
throw new ServerError("mod.upload.key.read");
}
}
let index = 0;
for (const file of files) {
const type = files.length === 1 ? "universal" : index === 0 ? "steam" : "oculus";
const filePath = `/uploads/${_id.toString()}/${type}/`;
const fileName = `${name}-${version}.zip`;
const fullPath = path.join(process.cwd(), filePath);
const fullFile = path.join(fullPath, fileName);
const fullSigFile = `${fullFile}.sig`;
try {
await new Promise((res, rej) => {
const mkdir = (dirPath: string, root = "") => {
Expand Down Expand Up @@ -274,6 +305,24 @@ export default class ModService {
console.error("ModService.create zip.read", error);
throw new ServerError("mod.upload.zip.corrupt");
}
try {
await new Promise((res, rej) => {
const signOptions = {
message: openpgp.message.fromBinary(fs.createReadStream(fullFile)),
privateKeys: [privkey],
detached: true,
streaming: "node"
};
openpgp.sign(signOptions).then(signed => {
signed.signature.pipe(fs.createWriteStream(fullSigFile));
openpgp.stream.readToEnd(signOptions.message.armor()).catch(err => rej(err));
res();
});
});
} catch (err) {
console.error("ModService.create", "SIG Create", err);
throw new ServerError("mod.upload.sig.create");
}
index++;
}
if (mod.downloads && !mod.downloads.length) {
Expand All @@ -286,3 +335,18 @@ export default class ModService {
return true;
}
}

async function readPrivkey() {
const pk: any = await new Promise((res, rej) => {
fs.readFile(path.join(process.cwd(), "/keys/privkey.asc"), "utf-8", (err, data) => {
if (err) {
rej(err);
}
openpgp.key.readArmored(data).then(output => {
res(output.keys[0]);
});
});
});
await pk.decrypt(process.env.PASSPHRASE);
return pk;
}
58 changes: 58 additions & 0 deletions src/utils/pgpKeygen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const fs = require("fs");
const path = require("path");
const openpgp = require("openpgp");
const rl = require("readline").createInterface({
input: process.stdin,
output: process.stdout
});

gen().catch(err => {throw err;});

function ask (question) {
return new Promise(res => {
rl.question(question, answer => {
res(answer);
});
});
}

function writeKey (filePath, fileKey, type) {
return new Promise((res, rej) => {
fs.writeFile(filePath, fileKey, err => {
if (err) {
rej(err);
}
console.log(` Done writing ${type} to ${filePath}`);
res();
});
});
}

async function gen () {
console.log("======== PGP Key Pair Generator ========");

console.log("- Awaiting info...");
let userId = {};
userId.name = await ask(" Name [Beat Saber Modding Group] : ") || "Beat Saber Modding Group";
userId.email = await ask(" Email [bsmg@beatmods.com] : ") || "bsmg@beatmods.com";
const passphrase = await ask(" Passphrase [q1w2e3r4t5y6] : ") || "q1w2e3r4t5y6";
let numBits = await ask(" 4096 bits ? (More secure, slower) [yes] : ");
numBits = numBits && (numBits.toLowerCase() === "n" || numBits.toLowerCase() === "no") ? 2048 : 4096;
const dir = await ask(" Output directory [./keys] : ") || path.join(process.cwd(), "keys");
rl.close();

const genOptions = {
userIds: [userId],
numBits: numBits,
passphrase: passphrase
};
console.log(`- Generating ${numBits} bits key pair with user ID ${userId.name} <${userId.email}>...`);
const key = await openpgp.generateKey(genOptions);

console.log("- Writing keys...");
await writeKey(path.join(dir, "privkey.asc"), key.privateKeyArmored, "private key");
await writeKey(path.join(dir, "pubkey.asc"), key.publicKeyArmored, "public key");
await writeKey(path.join(dir, "revcert.asc"), key.revocationCertificate, "revocation certificate");

console.log("========================================");
}