diff --git a/.gitignore b/.gitignore index 6de704e..aca2d31 100755 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ yarn.lock tunnel.sh uploads/* -images/* \ No newline at end of file +images/* +keys/* diff --git a/package.json b/package.json index df6b01a..7a3118e 100755 --- a/package.json +++ b/package.json @@ -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}": [ @@ -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", diff --git a/src/server/v1/index.ts b/src/server/v1/index.ts index 7a9f641..47dc5ab 100644 --- a/src/server/v1/index.ts +++ b/src/server/v1/index.ts @@ -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); diff --git a/src/server/v1/mod.ts b/src/server/v1/mod.ts index 64745db..94bcfee 100644 --- a/src/server/v1/mod.ts +++ b/src/server/v1/mod.ts @@ -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 }); @@ -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)); }) ); diff --git a/src/server/v1/modules/ModService.ts b/src/server/v1/modules/ModService.ts index 49975fd..d48638a 100644 --- a/src/server/v1/modules/ModService.ts +++ b/src/server/v1/modules/ModService.ts @@ -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 { @@ -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) { @@ -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) { @@ -210,6 +232,14 @@ 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"; @@ -217,6 +247,7 @@ export default class ModService { 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 = "") => { @@ -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) { @@ -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; +} diff --git a/src/utils/pgpKeygen.js b/src/utils/pgpKeygen.js new file mode 100644 index 0000000..67886ea --- /dev/null +++ b/src/utils/pgpKeygen.js @@ -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("========================================"); +}