identity/asset-api/src/index.js
2024-06-30 22:05:46 +02:00

201 lines
5.8 KiB
JavaScript

// Identity. Store your memories and mental belongings
// Copyright (C) 2024 Sofía Aritz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { readFile } from "node:fs/promises";
import { createWriteStream, mkdirSync, readFileSync, writeFileSync, existsSync } 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";
import cors from "@fastify/cors";
import {
M2M_ALGORITHM,
ASSETS_FOLDER,
ASSET_API_LANDING_MESSAGE,
IDENTITY_API_ENDPOINT,
PRIVATE_KEY_PATH,
PUBLIC_KEY_PATH,
} from "./consts.js";
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 app = new Fastify({
logger: true,
});
app.register(multipart);
app.register(cors, {
origin: true,
});
app.get("/", async () => {
return signString(ASSET_API_LANDING_MESSAGE);
});
app.get("/crypto/cert", async () => {
return M2M_PUBLIC_KEY;
});
app.get("/crypto/algo", () => {
return M2M_ALGORITHM;
});
app.put("/asset", {
async handler(request, reply) {
let { user, limits } = await userFromSessionKey(request.query.session_key);
if (user.assets.length >= limits.maxAssetCount) {
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"],
},
},
});
app.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"],
},
},
});
app.listen({ port: 3001 });
function loadM2MKeys() {
try {
return {
private: readFileSync(PRIVATE_KEY_PATH).toString("ascii"),
public: readFileSync(PUBLIC_KEY_PATH).toString("ascii"),
};
} catch {
console.warn("M2M key pair not found. Generating M2M key pair!");
let { publicKey, privateKey } = generateKeyPairSync("rsa", {
modulusLength: 4096,
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
let privateDir = join(PRIVATE_KEY_PATH, "..");
if (!existsSync(privateDir)) {
console.warn("The private key folder does not exist. It will be created.");
mkdirSync(privateDir, { recursive: true });
}
let publicDir = join(PUBLIC_KEY_PATH, "..");
if (!existsSync(publicDir)) {
console.warn("The public key folder does not exist. It will be created.");
mkdirSync(publicDir, { recursive: true });
}
writeFileSync(PRIVATE_KEY_PATH, privateKey);
writeFileSync(PUBLIC_KEY_PATH, 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();
}