Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

working prototype #2

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions example/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -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

6 changes: 3 additions & 3 deletions example/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> = new Map<string, any>();
Expand Down Expand Up @@ -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'
});
Expand All @@ -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');
});

Expand Down
3 changes: 1 addition & 2 deletions example/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
277 changes: 277 additions & 0 deletions example/backend/src/InitializeClass.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = new Map();
private prId: string;
private origin: string[];
private users: Map<string, User> = 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
};
28 changes: 28 additions & 0 deletions example/backend/src/functions.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
25 changes: 25 additions & 0 deletions example/backend/src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading