From a3bb1d7bcbc83ca23662d89a5804a71a5839452c Mon Sep 17 00:00:00 2001 From: rstp-god Date: Tue, 5 Dec 2023 15:53:15 +0300 Subject: [PATCH] working prototype --- example/backend/Dockerfile | 11 + example/backend/index.ts | 6 +- example/backend/package.json | 3 +- example/backend/src/InitializeClass.ts | 277 +++++++++++++++++++++++++ example/backend/src/functions.ts | 28 +++ example/backend/src/interfaces.ts | 25 +++ example/backend/storage.json | 50 +---- example/docker-compose.yml | 14 ++ example/frontend/Dockerfile | 11 + example/frontend/craco.config.js | 3 + example/frontend/nginx/nginx.conf | 25 +++ example/frontend/src/App.tsx | 2 +- 12 files changed, 400 insertions(+), 55 deletions(-) create mode 100644 example/backend/Dockerfile create mode 100644 example/backend/src/InitializeClass.ts create mode 100644 example/backend/src/functions.ts create mode 100644 example/backend/src/interfaces.ts create mode 100644 example/docker-compose.yml create mode 100644 example/frontend/Dockerfile create mode 100644 example/frontend/nginx/nginx.conf diff --git a/example/backend/Dockerfile b/example/backend/Dockerfile new file mode 100644 index 0000000..d449d2c --- /dev/null +++ b/example/backend/Dockerfile @@ -0,0 +1,11 @@ +# syntax=docker/dockerfile:1 +FROM node:18.16.0-alpine as build +RUN apk add --no-cache python3 g++ make +WORKDIR /backend +COPY package.json . +RUN yarn install +COPY . . +RUN yarn run build +CMD ["node", "--es-module-specifier-resolution=node", "dist/index.js"] +EXPOSE 3004 + diff --git a/example/backend/index.ts b/example/backend/index.ts index 5ea6c1f..7d38180 100644 --- a/example/backend/index.ts +++ b/example/backend/index.ts @@ -7,7 +7,7 @@ import bodyParser from 'body-parser'; import cors from 'cors'; import { KeyPairType } from 'virgil-crypto'; import * as fs from 'fs'; -import { ZtMiddleware } from '@virgilsecurity/virgil-zt'; +import { ZtMiddleware } from "./src/InitializeClass"; const TemplateStorage: Map = new Map(); @@ -35,7 +35,7 @@ const virgil = new ZtMiddleware({ registerPath: '/register', keyType: KeyPairType.ED25519, replayingId: 'localhost', - expectedOrigin: [ 'http://localhost:3000' ], + expectedOrigin: [ 'http://localhost:33435' ], storageControl: storage, encoding: 'base64' }); @@ -54,7 +54,7 @@ app.post('/new-post', (req: Request, res: Response) => { res.send({data: {name: 'Tester', password: 'pass'}}); }); -const server = app.listen(3002, () => { +const server = app.listen(33434, () => { console.log('Server is running'); }); diff --git a/example/backend/package.json b/example/backend/package.json index 92db8ff..9731859 100644 --- a/example/backend/package.json +++ b/example/backend/package.json @@ -8,7 +8,7 @@ "type": "module", "scripts": { "build": "tsc --project tsconfig.json", - "start": "node dist/index.js", + "start": "node --es-module-specifier-resolution=node dist/index.js", "dev": "tsc -watch", "dev:watch": "concurrently \"yarn dev\" \"nodemon --exec ts-node -q dist/index.js\"", "lint:fix": "eslint . --ext .ts --fix" @@ -17,7 +17,6 @@ "@simplewebauthn/server": "^8.3.5", "@types/cors": "^2.8.13", "@virgilsecurity/data-utils": "^2.0.0", - "@virgilsecurity/virgil-zt": "^1.0.8", "base64url": "^3.0.1", "body-parser": "^1.20.2", "cors": "^2.8.5", diff --git a/example/backend/src/InitializeClass.ts b/example/backend/src/InitializeClass.ts new file mode 100644 index 0000000..c13a926 --- /dev/null +++ b/example/backend/src/InitializeClass.ts @@ -0,0 +1,277 @@ +import { + initCrypto, + VirgilCrypto +} from 'virgil-crypto'; +import { + CryptoKeys, + Settings, + User +} from './interfaces'; +import { + Request, + Response +} from 'express'; +import { NodeBuffer } from '@virgilsecurity/data-utils'; +import { VirgilPublicKey } from 'virgil-crypto/dist/types/VirgilPublicKey'; +import { + verifyAuthenticationResponse, + verifyRegistrationResponse +} from '@simplewebauthn/server'; +import { + getRegistrationInfo, + getSavedAuthenticatorData +} from './functions'; +import base64url from 'base64url'; + + +class ZtMiddleware { + + private encryptKeys: CryptoKeys; + private virgilCrypto: VirgilCrypto; + private frontendPublicKey: VirgilPublicKey; + private loginPath: string; + private registerPath: string; + private encryptEncoding: BufferEncoding; + private storageControl: CallableFunction; + private activeStorage = false; + + + //Passkeys Flow variables + private passkeysActive: boolean; + private challenges: Map = new Map(); + private prId: string; + private origin: string[]; + private users: Map = new Map(); + + /* + Initialize crypto module to upload wasm and other files + */ + private static initializeCryptoModule = async () => { + await initCrypto(); + }; + + constructor(settings: Settings) { + ZtMiddleware.initializeCryptoModule() + .then(() => { + const { + replayingId, + expectedOrigin, + registerPath, + loginPath, + keyType, + encoding = 'base64', + storageControl + } = settings; + this.prId = replayingId ?? ''; + this.origin = expectedOrigin ?? ''; + this.registerPath = registerPath; + this.loginPath = loginPath; + this.virgilCrypto = new VirgilCrypto({defaultKeyPairType: keyType}); + this.encryptKeys = this.virgilCrypto.generateKeys(); + this.loginPath = loginPath; + this.encryptEncoding = encoding; + if (storageControl) { + this.storageControl = storageControl; + const serverKeys = this.storageControl(false, false); + if (serverKeys) { + this.encryptKeys = serverKeys; + } else { + this.storageControl(true, false, this.encryptKeys); + } + this.activeStorage = true; + } + console.log('Successfully init Crypto Module'); + }); + } + + private encrypt(data: string): string { + return this.virgilCrypto.signThenEncrypt(data, this.encryptKeys.privateKey, [ this.encryptKeys.publicKey, this.frontendPublicKey ]) + .toString(this.encryptEncoding); + } + + private decrypt(data: string): string { + return this.virgilCrypto.decryptThenVerify(data, this.encryptKeys.privateKey, [ this.encryptKeys.publicKey, this.frontendPublicKey ]) + .toString('utf-8'); + } + + private setKey(key: string): void { + if (this.activeStorage) { + const getKey = this.storageControl(false, true); + if (getKey) { + this.frontendPublicKey = this.virgilCrypto.importPublicKey(NodeBuffer.from(getKey, 'base64')); + } else { + this.storageControl(true, true, key); + this.frontendPublicKey = this.virgilCrypto.importPublicKey(NodeBuffer.from(key, 'base64')); + } + return; + } + this.frontendPublicKey = this.virgilCrypto.importPublicKey(NodeBuffer.from(key, 'base64')); + } + + private rewriteResponse(res: Response, pubKey?: unknown) { + const oldJson = res.json; + res.json = (body) => { + res.locals.body = body; + if (this.passkeysActive) { + body = {data: pubKey}; + } else { + body = {data: this.encrypt(typeof (body.data) === 'string' ? body.data : JSON.stringify(body.data))}; + } + return oldJson.call(res, body); + }; + } + + private postFlow(req: Request, res: Response) { + if (req.body.data) { + req.body.data = JSON.parse(this.decrypt(req.body.data)); + } + this.rewriteResponse(res); + } + + private defaultFlow(res: Response) { + this.rewriteResponse(res); + } + + private getNewChallenge(): string { + return Math.random() + .toString(36) + .substring(2); + } + + private convertChallenge(challenge: string) { + return btoa(challenge) + .replaceAll('=', ''); + } + + private async loginFlow(req: Request, res: Response, next: CallableFunction) { + switch (req.url) { + case this.registerPath + '/start': { + const username = req.body.username; + const challenge = this.getNewChallenge(); + const newId = this.users.size + 1; + this.challenges.set(username, this.convertChallenge(challenge)); + const pubKey = { + challenge: challenge, + rp: {id: this.prId, name: 'webauthn-app'}, + user: {id: newId, name: username, displayName: username}, + pubKeyCredParams: [ + {type: 'public-key', alg: -7}, + {type: 'public-key', alg: -257}, + ], + authenticatorSelection: { + authenticatorAttachment: 'cross-platform', + userVerification: 'discouraged', + residentKey: 'discouraged', + requireResidentKey: false, + } + }; + res.send({data: pubKey}); + res.status(200); + return next(); + } + case this.registerPath + '/finish': { + const username = req.body.username; + await verifyRegistrationResponse({ + response: req.body.data, + expectedChallenge: this.challenges.get(username)!, + expectedOrigin: this.origin + }) + .then((result) => { + const {verified, registrationInfo} = result; + if (verified) { + this.users.set(username, getRegistrationInfo(registrationInfo)); + res.send({data: verified}); + res.status(200); + return next(); + } + }) + .catch((error) => { + console.error(error); + res.status(400); + return next(); + }); + res.status(500); + return next(); + } + case this.loginPath + '/start': { + const username = req.body.username; + if (!this.users.get(username)) { + res.status(404); + return next(); + } + const challenge = this.getNewChallenge(); + this.challenges.set(username, this.convertChallenge(challenge)); + res.send({ + data: { + challenge, + rpId: this.prId, + allowCredentials: [ { + type: 'public-key', + id: this.users.get(username)!.credentialID, + transports: [ 'external' ], + } ], + userVerification: 'discouraged', + serverKey: this.virgilCrypto.exportPublicKey(this.encryptKeys.publicKey) + .toString('base64') + } + }); + res.status(200); + return next(); + } + case this.loginPath + '/finish': { + const username = req.body.data.username; + if (!this.users.get(username)) { + return res.status(404) + .send(false); + } + const user = this.users.get(username); + const clientInfoObj = JSON.parse(base64url.decode(req.body.data.data.response.clientDataJSON)); + const concatChallenge = base64url.decode(clientInfoObj.challenge); + const key = concatChallenge.slice(concatChallenge.indexOf('_') + 1); + this.setKey(key); + await verifyAuthenticationResponse({ + expectedChallenge: base64url(base64url.decode(this.challenges.get(username)!) + '_' + key), + response: req.body.data.data, + authenticator: getSavedAuthenticatorData(user), + expectedRPID: this.prId, + expectedOrigin: this.origin + }) + .then((result) => { + const {verified} = result; + res.send({res: verified}); + res.status(200); + return next(); + }) + .catch((error) => { + console.error(error); + res.status(400); + return next(); + }); + res.status(500); + return next(); + } + default: + return next(); + } + } + + public zeroTrustMiddleware = async (req: Request, res: Response, next: CallableFunction) => { + if ((req.url.split('/') + .includes(this.loginPath.slice(1)) || req.url.split('/') + .includes(this.registerPath.slice(1))) && req.method === 'POST') { + return this.loginFlow(req, res, next); + } + if (req.method === 'POST' || req.method === 'PUT') { + this.postFlow(req, res); + return next(); + } + this.defaultFlow(res); + return next(); + }; + +} + + +export { + ZtMiddleware +}; diff --git a/example/backend/src/functions.ts b/example/backend/src/functions.ts new file mode 100644 index 0000000..bb97db9 --- /dev/null +++ b/example/backend/src/functions.ts @@ -0,0 +1,28 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +export function uintToString(a) { + const base64string = btoa(String.fromCharCode(...a)); + return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); +} + +export function base64ToUint8Array(str) { + str = str.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, ''); + return new Uint8Array(Array.prototype.map.call(atob(str), (c) => c.charCodeAt(0))); +} + +export function getSavedAuthenticatorData(user) { + return { + credentialID: base64ToUint8Array(user.credentialID), + credentialPublicKey: base64ToUint8Array(user.credentialPublicKey), + counter: user.counter, + }; +} + +export function getRegistrationInfo(registrationInfo) { + const {credentialPublicKey, counter, credentialID} = registrationInfo; + return { + credentialID: uintToString(credentialID), + credentialPublicKey: uintToString(credentialPublicKey), + counter, + }; +} diff --git a/example/backend/src/interfaces.ts b/example/backend/src/interfaces.ts new file mode 100644 index 0000000..e9c6e8a --- /dev/null +++ b/example/backend/src/interfaces.ts @@ -0,0 +1,25 @@ +import { VirgilPrivateKey } from 'virgil-crypto/dist/types/VirgilPrivateKey'; +import { VirgilPublicKey } from 'virgil-crypto/dist/types/VirgilPublicKey'; +import { KeyPairType } from 'virgil-crypto'; + + +export interface CryptoKeys { + privateKey: VirgilPrivateKey; + publicKey: VirgilPublicKey; +} + +export interface Settings { + loginPath: string, + registerPath: string, + keyType: KeyPairType, + replayingId: string, + expectedOrigin: string[], + storageControl?: CallableFunction, + encoding?: BufferEncoding +} + +export interface User { + credentialID: string; + credentialPublicKey: string; + counter: unknown; +} diff --git a/example/backend/storage.json b/example/backend/storage.json index 9f3e423..a94b960 100644 --- a/example/backend/storage.json +++ b/example/backend/storage.json @@ -1,49 +1 @@ -{ - "serverKeys": [ - { - "privateKey": { - "identifier": { - "type": "Buffer", - "data": [ - 171, - 209, - 34, - 95, - 6, - 146, - 16, - 227 - ] - }, - "lowLevelPrivateKey": { - "name": "RawPrivateKey", - "ctxPtr": 250560 - }, - "_isDisposed": false - }, - "publicKey": { - "identifier": { - "type": "Buffer", - "data": [ - 171, - 209, - 34, - 95, - 6, - 146, - 16, - 227 - ] - }, - "lowLevelPublicKey": { - "name": "RawPublicKey", - "ctxPtr": 250536 - }, - "_isDisposed": false - } - } - ], - "clientKeys": [ - "MCowBQYDK2VwAyEAwjXYyDPUOr2z9RRo6NRUuO9io4M9DBn7vPz/HJxtCMY=" - ] -} +{"serverKeys":[{"privateKey":{"identifier":{"type":"Buffer","data":[95,22,156,59,13,197,4,113]},"lowLevelPrivateKey":{"name":"RawPrivateKey","ctxPtr":250560},"_isDisposed":false},"publicKey":{"identifier":{"type":"Buffer","data":[95,22,156,59,13,197,4,113]},"lowLevelPublicKey":{"name":"RawPublicKey","ctxPtr":250536},"_isDisposed":false}}],"clientKeys":["MCowBQYDK2VwAyEA7nTIuUQwrNAwmDpcDCmD4UsGjUda8xyKf71UsrAtEI0="]} \ No newline at end of file diff --git a/example/docker-compose.yml b/example/docker-compose.yml new file mode 100644 index 0000000..98a3da5 --- /dev/null +++ b/example/docker-compose.yml @@ -0,0 +1,14 @@ +version : "1" +services: + backend: + build: ./backend + container_name: backend + environment: + - NODE_OPTIONS="--experimental-specifier-resolution=node" + ports: + - 33434:33434 + frontend: + build: ./frontend + container_name: frontend + ports: + - 33435:80 diff --git a/example/frontend/Dockerfile b/example/frontend/Dockerfile new file mode 100644 index 0000000..64ad1da --- /dev/null +++ b/example/frontend/Dockerfile @@ -0,0 +1,11 @@ +# syntax=docker/dockerfile:1 +FROM node:18.16.0-alpine as build +RUN apk add --no-cache python3 g++ make +WORKDIR /frontend +COPY package*.json . +RUN yarn install +COPY . . +RUN yarn run build +FROM nginx:1.19 +COPY ./nginx/nginx.conf /etc/nginx/nginx.conf +COPY --from=build /frontend/build /usr/share/nginx/html diff --git a/example/frontend/craco.config.js b/example/frontend/craco.config.js index e52b194..d814e10 100644 --- a/example/frontend/craco.config.js +++ b/example/frontend/craco.config.js @@ -13,4 +13,7 @@ module.exports = { }, }, }, + devServer: { + port: 33435 + } }; diff --git a/example/frontend/nginx/nginx.conf b/example/frontend/nginx/nginx.conf new file mode 100644 index 0000000..8b58687 --- /dev/null +++ b/example/frontend/nginx/nginx.conf @@ -0,0 +1,25 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html index.htm; + include /etc/nginx/mime.types; + + gzip on; + gzip_min_length 1000; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; + + location / { + try_files $uri $uri/ /index.html; + } + } +} diff --git a/example/frontend/src/App.tsx b/example/frontend/src/App.tsx index 1851a87..12e65e8 100644 --- a/example/frontend/src/App.tsx +++ b/example/frontend/src/App.tsx @@ -28,7 +28,7 @@ init() }); const request = new Axios({ - baseURL: 'http://localhost:3002', + baseURL: 'http://' + new URL(window.location.href).host.slice(0, new URL(window.location.href).host.indexOf(':')) + ':33434', transformRequest: (req) => JSON.stringify(req), transformResponse: (res) => JSON.parse(res), headers: {