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 (request, reply) => { return signString(ASSET_API_LANDING_MESSAGE) }) fastify.get("/crypto/cert", async (request, reply) => { return M2M_PUBLIC_KEY }) fastify.get("/crypto/algo", (request, reply) => { return M2M_ALGORITHM }) fastify.put("/asset", { async handler(request, reply) { await userFromSessionKey(request.query.session_key) 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, }) }) promisify(pipeline)(file.file, createWriteStream(`.assets/${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 (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 { 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 (e) { 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() }