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 (request, reply) => { 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, reply) { 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, reply) { 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] }