From a1784f5cb25319e534fdc499c60fe0511f1191ec Mon Sep 17 00:00:00 2001 From: Valerie Date: Tue, 26 Jul 2022 21:05:26 -0400 Subject: [PATCH] totp in database --- package-lock.json | 60 +++++++++++++++ package.json | 3 + packages/preload/src/index.ts | 2 +- packages/renderer/src/components/Totp.tsx | 75 +++++++++++++++++++ packages/renderer/src/pages/App.tsx | 2 - packages/renderer/src/pages/Channels.tsx | 2 + packages/server/public/migrations/7-2fa.sql | 2 + .../server/src/db/snippets/totp/confirm.sql | 3 + packages/server/src/index.ts | 2 + packages/server/src/routers/totp.ts | 70 +++++++++++++++++ packages/server/tsconfig.json | 2 +- 11 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 packages/renderer/src/components/Totp.tsx create mode 100644 packages/server/public/migrations/7-2fa.sql create mode 100644 packages/server/src/db/snippets/totp/confirm.sql create mode 100644 packages/server/src/routers/totp.ts diff --git a/package-lock.json b/package-lock.json index 7c8107c..99fbdc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@types/react-timeago": "^4.1.3", + "@types/totp-generator": "^0.0.4", "@types/uuid": "^8.3.4", "@types/ws": "^8.5.3", "@vitejs/plugin-react": "^2.0.0", @@ -27,6 +28,8 @@ "react-icons": "^4.4.0", "react-time-ago": "^7.2.1", "react-timeago": "^7.1.0", + "reactjs-popup": "^2.0.5", + "totp-generator": "^0.0.13", "uuid": "^8.3.2", "vue": "3.2.37", "ws": "^8.8.1" @@ -1111,6 +1114,11 @@ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==" }, + "node_modules/@types/totp-generator": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/totp-generator/-/totp-generator-0.0.4.tgz", + "integrity": "sha512-rs5nfg//Q6zXGRiIqZ6UUhh65lBgEM5/s8knGw3fO2TBa0i1GlSLx4+P45vXNTrp3uNnqdSS1z4I33Oi3egDDg==" + }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -6833,6 +6841,14 @@ "extsprintf": "^1.2.0" } }, + "node_modules/jssha": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", + "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", + "engines": { + "node": "*" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.2.tgz", @@ -8643,6 +8659,18 @@ "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/reactjs-popup": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.5.tgz", + "integrity": "sha512-b5hv9a6aGsHEHXFAgPO5s1Jw1eSkopueyUVxQewGdLgqk2eW0IVXZrPRpHR629YcgIpC2oxtX8OOZ8a7bQJbxA==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -9878,6 +9906,14 @@ "node": ">=0.6" } }, + "node_modules/totp-generator": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/totp-generator/-/totp-generator-0.0.13.tgz", + "integrity": "sha512-/Sg4eXvaLfaXNJJWv/OLPQm1GVKQXOe6mbDNXFBRZdYnaN9iY6Grk6uPYLph9MDn+cGn6GLNaqpak34jfu1CQQ==", + "dependencies": { + "jssha": "^3.1.2" + } + }, "node_modules/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -11701,6 +11737,11 @@ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==" }, + "@types/totp-generator": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/totp-generator/-/totp-generator-0.0.4.tgz", + "integrity": "sha512-rs5nfg//Q6zXGRiIqZ6UUhh65lBgEM5/s8knGw3fO2TBa0i1GlSLx4+P45vXNTrp3uNnqdSS1z4I33Oi3egDDg==" + }, "@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -15931,6 +15972,11 @@ } } }, + "jssha": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", + "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==" + }, "jsx-ast-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.2.tgz", @@ -17280,6 +17326,12 @@ "integrity": "sha512-rouF7MiEm55fH791Y8cg+VobIJgx8gtNJ+gjr86R4ZqO1WKPkXiXjdT/lRzrvEkUzsxT1exHqV2V+Zdi114H3A==", "requires": {} }, + "reactjs-popup": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.5.tgz", + "integrity": "sha512-b5hv9a6aGsHEHXFAgPO5s1Jw1eSkopueyUVxQewGdLgqk2eW0IVXZrPRpHR629YcgIpC2oxtX8OOZ8a7bQJbxA==", + "requires": {} + }, "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -18214,6 +18266,14 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "totp-generator": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/totp-generator/-/totp-generator-0.0.13.tgz", + "integrity": "sha512-/Sg4eXvaLfaXNJJWv/OLPQm1GVKQXOe6mbDNXFBRZdYnaN9iY6Grk6uPYLph9MDn+cGn6GLNaqpak34jfu1CQQ==", + "requires": { + "jssha": "^3.1.2" + } + }, "tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", diff --git a/package.json b/package.json index bd7c3c1..bf95b2c 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@types/react-timeago": "^4.1.3", + "@types/totp-generator": "^0.0.4", "@types/uuid": "^8.3.4", "@types/ws": "^8.5.3", "@vitejs/plugin-react": "^2.0.0", @@ -76,6 +77,8 @@ "react-icons": "^4.4.0", "react-time-ago": "^7.2.1", "react-timeago": "^7.1.0", + "reactjs-popup": "^2.0.5", + "totp-generator": "^0.0.13", "uuid": "^8.3.2", "vue": "3.2.37", "ws": "^8.8.1" diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index 2383d11..709e13c 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -6,6 +6,6 @@ export { getClientId, setClientId, getHomeServer, - setHomeServer, + setHomeServer } from './settings'; export {versions} from './versions'; diff --git a/packages/renderer/src/components/Totp.tsx b/packages/renderer/src/components/Totp.tsx new file mode 100644 index 0000000..f52aa7e --- /dev/null +++ b/packages/renderer/src/components/Totp.tsx @@ -0,0 +1,75 @@ +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import 'reactjs-popup/dist/index.css'; +import { useApi } from '../lib/useApi'; +import { ClientIdContext } from '../pages/App'; +import QR from 'qrcode'; + +function usePrevious(value: any) { + // The ref object is a generic container whose current property is mutable ... + // ... and can hold any value, similar to an instance property on a class + const ref = useRef(); + // Store current value in ref + useEffect(() => { + ref.current = value; + }, [value]); // Only re-run if value changes + // Return previous value (happens before update in useEffect above) + return ref.current; +} + +export default function Totp () { + + const [open, setOpen] = useState(false); + const previousOpen = usePrevious(open); + const { clientId } = useContext(ClientIdContext); + const [qr, setQr] = useState(null); + const [key, setKey] = useState(''); + + const { send } = useApi({ + 'totp:propose'(key: string) { + setKey(key); + } + }, []); + + const onOpen = useCallback(() => { + send('totp:propose', clientId); + }, [send, clientId]); + + useEffect(() => { + if(open && !previousOpen) { + onOpen(); + } + }, [open, onOpen]); + + useEffect(() => { + if(key === null) return; + if(clientId === null) return; + (async () => { + setQr(await QR.toDataURL( + 'otpauth://totp/Valerie?secret=' + + key + + '&issuer=corner' + )); + })() + }, [key, clientId]) + + const inputRef = useRef(null); + + const submit = useCallback(() => { + if(inputRef.current === null) return; + send('totp:confirm', { + clientId, + code: inputRef.current.value + }) + }, []) + + return
+ + {open && ( +
+ + + +
+ )} +
+} \ No newline at end of file diff --git a/packages/renderer/src/pages/App.tsx b/packages/renderer/src/pages/App.tsx index a473681..f86c817 100644 --- a/packages/renderer/src/pages/App.tsx +++ b/packages/renderer/src/pages/App.tsx @@ -80,8 +80,6 @@ export default function App() { // font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; // color: #f8f8f2; // background: #282a36; - - return ( diff --git a/packages/renderer/src/pages/Channels.tsx b/packages/renderer/src/pages/Channels.tsx index 486fc32..e5ef6d0 100644 --- a/packages/renderer/src/pages/Channels.tsx +++ b/packages/renderer/src/pages/Channels.tsx @@ -4,6 +4,7 @@ import { useApi } from '../lib/useApi'; import type { IMessage } from './Message'; import NameTextbox from './NameTextbox'; import LoginQR from './LoginQR'; +import Totp from '../components/Totp'; interface IChannel { uid: string; @@ -143,6 +144,7 @@ export default function Channels() {



+ ); } \ No newline at end of file diff --git a/packages/server/public/migrations/7-2fa.sql b/packages/server/public/migrations/7-2fa.sql new file mode 100644 index 0000000..b611fd0 --- /dev/null +++ b/packages/server/public/migrations/7-2fa.sql @@ -0,0 +1,2 @@ +ALTER TABLE `clients` +ADD `totp` varchar(16) COLLATE 'utf8mb4_general_ci' NULL; \ No newline at end of file diff --git a/packages/server/src/db/snippets/totp/confirm.sql b/packages/server/src/db/snippets/totp/confirm.sql new file mode 100644 index 0000000..65f9f62 --- /dev/null +++ b/packages/server/src/db/snippets/totp/confirm.sql @@ -0,0 +1,3 @@ +UPDATE clients +SET totp=? +WHERE uid=?; \ No newline at end of file diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 62b6173..6045357 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -4,6 +4,7 @@ import { expose } from './lib/WebSocketServer'; import message from './routers/message'; import channel from './routers/channel'; import client from './routers/client'; +import totp from './routers/totp'; const api = router({ up() { @@ -15,6 +16,7 @@ const api = router({ channels: channel, client: client, clients: client, + totp: totp, }); expose(api, 3000); diff --git a/packages/server/src/routers/totp.ts b/packages/server/src/routers/totp.ts new file mode 100644 index 0000000..f26618d --- /dev/null +++ b/packages/server/src/routers/totp.ts @@ -0,0 +1,70 @@ +import router from "../lib/router" +import { reply } from "../lib/WebSocketServer" +import { randomBytes } from 'crypto'; +import getToken from 'totp-generator'; +import query from "../db/query"; +import confirm from '../db/snippets/totp/confirm.sql'; +// 0 1 2 3 4 +// | | | | | | +// 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 +// 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 +// | | | | | | | | | +// a b c d e f g h +const mask = (len: number) => Math.pow(2, len) - 1; + +const manipulate = (b: number, start: number, len: number, end: number) => + (((b >> start) & mask(len)) << end) & (mask(len) << end) + +const dict = (n: number): string => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.at(n) as string; + +function rb32() { + const bytes = randomBytes(5); + + const a = manipulate(bytes[0], 3, 5, 0); + const b = manipulate(bytes[0], 0, 3, 2) | manipulate(bytes[1], 6, 2, 0); + const c = manipulate(bytes[1], 1, 5, 0); + const d = manipulate(bytes[1], 0, 1, 4) | manipulate(bytes[2], 4, 4, 0); + const e = manipulate(bytes[2], 0, 4, 1) | manipulate(bytes[3], 7, 1, 0); + const f = manipulate(bytes[3], 2, 5, 0); + const g = manipulate(bytes[3], 0, 2, 3) | manipulate(bytes[4], 5, 3, 0); + const h = manipulate(bytes[4], 0, 5, 0); + + return dict(a) + dict(b) + dict(c) + dict(d) + dict(e) + dict(f) + dict(g) + dict(h); +} + +const totpKey = () => rb32() + rb32(); + +const proposals: any = {} + +export default router({ + 'propose'(clientId: string) { + if(clientId in proposals) return reply(proposals[clientId]); + const key = totpKey(); + proposals[clientId] = key; + console.log(clientId, proposals) + setTimeout(() => { + delete proposals[clientId]; + }, 5 * 60 * 1000); + console.log('created totp proposal'); + return reply(key) + }, + async 'confirm'(data: any) { + const { clientId, code } = data; + const key = proposals[clientId]; + const trueCode = getToken(key); + if(trueCode !== code) return reply({ + err: 'codes did not match!' + }); + + // add to database! + + const response = await query(confirm, key, clientId); + if(response === null) return reply({ + err: 'unknown database error, contact server admin.' + }); + + return reply({ + err: null + }); + } +}) \ No newline at end of file diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 97dc3a2..da6cbf8 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -7,7 +7,7 @@ "skipLibCheck": true, "strict": true, "isolatedModules": true, - + "allowSyntheticDefaultImports": true, "types" : ["node"], "baseUrl": ".",