import { createVerify } from "node:crypto"; import Fastify from "fastify"; import * as Jose from "jose"; import { v4 as uuidv4 } from "uuid"; import assert from "node:assert"; const IDENTITY_API_LANDING_MESSAGE = "identity-api v1.0.0"; const JWT_SECRET = new TextEncoder().encode("cc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9a95f2f596f2"); const JWT_ALG = "HS256"; const ASSET_API_ENDPOINT = "http://localhost:3001/"; let ASSET_API_PUBKEY = await loadAssetPubkey(); let ASSET_API_ALGORITHM = await loadAssetAlgo(); setInterval(async () => { try { let pubkey = await loadAssetPubkey(); let algo = await loadAssetAlgo(); if (pubkey != null && algo != null) { if (ASSET_API_PUBKEY !== pubkey) { console.warn("The M2M public key has changed!"); } if (ASSET_API_ALGORITHM !== algo) { console.warn("The M2M algorith has changed!"); } ASSET_API_PUBKEY = pubkey; ASSET_API_ALGORITHM = algo; console.debug("Successfully updated the M2M public key and algorithm"); } } catch (e) { console.warn("Failed to update the M2M public key", e); } }, 60 * 1000); let session_keys = { "uid:005d6417-a23c-48bd-b348-eafeae649b94": "e381ba8c-e18a-4bca-afce-b212b37bc26b", "key:e381ba8c-e18a-4bca-afce-b212b37bc26b": "005d6417-a23c-48bd-b348-eafeae649b94", }; let users = { "jane@identity.net": { uid: "005d6417-a23c-48bd-b348-eafeae649b94", email: "jane@identity.net", password: "12345678901234567890", name: "Jane Doe", assets: ["f9d040d6-598c-4483-952f-08e7d35d5420.jpg"], }, }; const fastify = Fastify({ logger: true, }); fastify.get("/", async () => { return IDENTITY_API_LANDING_MESSAGE; }); fastify.put("/m2m/asset", { async handler(request, reply) { if (!verifySignature(request.body, ASSET_API_PUBKEY)) { reply.statusCode(401); return; } let body = JSON.parse(getContentFromSigned(request.body)); let uid = session_keys[`key:${body.session_key}`]; let user = Object.values(users).filter((v) => v.uid === uid); assert(user.length === 1); users[user[0].email].assets.push(body.asset_id); console.log(users[user[0].email].assets); }, }); fastify.post("/m2m/account", { async handler(request, reply) { if (!verifySignature(request.body, ASSET_API_PUBKEY)) { reply.statusCode(401); return; } let body = JSON.parse(getContentFromSigned(request.body)); let uid = session_keys[`key:${body.session_key}`]; let user = Object.values(users).filter((v) => v.uid === uid); assert(user.length === 1); user[0].password = undefined; return user[0]; }, }); fastify.get("/auth/genkey", { async handler(request) { let jwt = request.headers["authorization"].replace("Bearer", "").trim(); let { payload } = await Jose.jwtVerify(jwt, JWT_SECRET); let key = uuidv4(); session_keys[`uid:${payload.uid}`] = key; session_keys[`key:${key}`] = payload.uid; return { session_key: session_keys[payload.uid], }; }, schema: { headers: { type: "object", properties: { authorization: { type: "string" }, }, }, }, }); fastify.get("/auth/account", { async handler(request) { let jwt = request.headers["authorization"].replace("Bearer", "").trim(); let { payload } = await Jose.jwtVerify(jwt, JWT_SECRET); let user = users[payload.email]; user.password = undefined; return user; }, schema: { headers: { type: "object", properties: { authorization: { type: "string" }, }, }, }, }); fastify.post("/auth/login", { async handler(request, reply) { let user = users[request.body.email]; if (user != null && user.password == request.body.password) { let token = await new Jose.SignJWT({ uid: user.uid, email: request.body.email, name: user.name, }) .setProtectedHeader({ alg: JWT_ALG }) .setIssuedAt() .setExpirationTime("4w") .sign(JWT_SECRET); return { token, }; } reply.code(400); return { error: "invalid credentials", }; }, schema: { body: { type: "object", properties: { email: { type: "string" }, password: { type: "string" }, }, }, }, }); fastify.post("/auth/register", { async handler(request, reply) { if (users[request.body.email] == null) { users[request.body.email] = { uid: uuidv4(), email: request.body.email, password: request.body.password, name: request.body.name, assets: [], }; let user = users[request.body.email]; let token = await new Jose.SignJWT({ uid: user.uid, email: request.body.email, name: user.name, }) .setProtectedHeader({ alg: JWT_ALG }) .setIssuedAt() .setExpirationTime("4w") .sign(JWT_SECRET); return { token }; } reply.code(400); return { error: "invalid data", }; }, schema: { body: { type: "object", properties: { name: { type: "string" }, email: { type: "string" }, password: { type: "string" }, }, }, }, }); fastify.listen({ port: 3000 }); async function loadAssetPubkey() { let url = new URL(ASSET_API_ENDPOINT); url.pathname = "/crypto/cert"; let res = await fetch(url); return await res.text(); } async function loadAssetAlgo() { let url = new URL(ASSET_API_ENDPOINT); url.pathname = "/crypto/algo"; let res = await fetch(url); return await res.text(); } /** * * @param {string} content * @param {string} pubkeyText */ function verifySignature(content, pubkeyText) { let parts = content .replace("-----BEGIN SIGNED MESSAGE-----\n\n", "") .replace("\n-----END SIGNATURE-----", "") .split("\n\n-----BEGIN SIGNATURE-----\n\n"); assert(parts.length === 2); let verify = createVerify(ASSET_API_ALGORITHM); verify.update(parts[0]); let pubkey = Buffer.from(pubkeyText, "ascii"); let digest = Buffer.from(parts[1], "base64"); return verify.verify(pubkey, digest); } function getContentFromSigned(content) { let parts = content .replace("-----BEGIN SIGNED MESSAGE-----\n\n", "") .replace("\n-----END SIGNATURE-----", "") .split("\n\n-----BEGIN SIGNATURE-----\n\n"); assert(parts.length === 2); return parts[0]; }