import { readFile } from "node:fs/promises"; import { createWriteStream, readFileSync, writeFileSync } from "node:fs"; import { createSign, generateKeyPairSync, randomUUID } from "node:crypto"; import Fastify from "fastify"; import multipart from "@fastify/multipart"; import { join } from "node:path"; import mime from "mime"; import { promisify } from "node:util"; import { pipeline } from "node:stream"; const M2M_ALGORITHM = "RSA-SHA512"; const { private: M2M_PRIVATE_KEY, public: M2M_PUBLIC_KEY } = loadM2MKeys(); if (M2M_PRIVATE_KEY == null || M2M_PUBLIC_KEY == null) { console.error("Couldn't load keys"); process.exit(1); } const ASSETS_FOLDER = "./.assets/"; const ASSET_API_LANDING_MESSAGE = "asset-api v1.0.0"; const IDENTITY_API_ENDPOINT = "http://localhost:3000"; const fastify = new Fastify({ logger: true, }); fastify.register(multipart); fastify.get("/", async () => { return signString(ASSET_API_LANDING_MESSAGE); }); fastify.get("/crypto/cert", async () => { return M2M_PUBLIC_KEY; }); fastify.get("/crypto/algo", () => { return M2M_ALGORITHM; }); fastify.put("/asset", { async handler(request, reply) { let user = await userFromSessionKey(request.query.session_key); if (user.assets.length >= user.limits.assetCount) { reply.code(403); return "Max asset count reached. Contact support or upgrade your plan"; } let file = await request.file(); let id = randomUUID().toString(); let extension = mime.getExtension(file.mimetype) || ".bin"; let full_id = `${id}.${extension}`; let url = new URL(IDENTITY_API_ENDPOINT); url.pathname = "/m2m/asset"; await fetch(url, { method: "PUT", body: signObject({ session_key: request.query.session_key, asset_id: full_id, }), }); await promisify(pipeline)(file.file, createWriteStream(`.assets/${full_id}`)); return { asset_id: full_id, } }, schema: { query: { type: "object", properties: { session_key: { type: "string" }, }, required: ["session_key"], }, }, }); fastify.get("/asset", { async handler(request, reply) { let user = await userFromSessionKey(request.query.session_key); if ('statusCode' in user) { reply.code(500); return "Something failed, please try again later" } if (user.assets.includes(request.query.asset_id)) { let path = join(ASSETS_FOLDER, request.query.asset_id); reply.type(mime.getType(path)); reply.send(await readFile(path)); } else { reply.code(401); return "Not authorized"; } }, schema: { query: { type: "object", properties: { asset_id: { type: "string" }, session_key: { type: "string" }, }, required: ["asset_id", "session_key"], }, }, }); fastify.listen({ port: 3001 }); function loadM2MKeys() { try { return { private: readFileSync("./.keys/m2m.pem").toString("ascii"), public: readFileSync("./.keys/m2m.pub").toString("ascii"), }; } catch { console.warn("Generating M2M key pair!"); let { publicKey, privateKey } = generateKeyPairSync("rsa", { modulusLength: 4096, publicKeyEncoding: { type: "spki", format: "pem", }, privateKeyEncoding: { type: "pkcs8", format: "pem", }, }); writeFileSync("./.keys/m2m.pem", privateKey); writeFileSync("./.keys/m2m.pub", publicKey); return loadM2MKeys(); } } function signString(content) { let sign = createSign(M2M_ALGORITHM); sign.update(content); return `-----BEGIN SIGNED MESSAGE-----\n\n${content}\n\n-----BEGIN SIGNATURE-----\n\n${sign.sign(M2M_PRIVATE_KEY, "base64")}\n-----END SIGNATURE-----`; } function signObject(content) { return signString(JSON.stringify(content)); } async function userFromSessionKey(session_key) { let url = new URL(IDENTITY_API_ENDPOINT); url.pathname = "/m2m/account"; let res1 = await fetch(url, { method: "POST", body: signObject({ session_key, }), }); return await res1.json(); }