totp in database

main
Valerie 2022-07-26 21:05:26 -04:00
parent 6b496cc045
commit a1784f5cb2
11 changed files with 219 additions and 4 deletions

60
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -6,6 +6,6 @@ export {
getClientId,
setClientId,
getHomeServer,
setHomeServer,
setHomeServer
} from './settings';
export {versions} from './versions';

View File

@ -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<any>();
// 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<string | null>(null);
const [key, setKey] = useState<string>('');
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<HTMLInputElement>(null);
const submit = useCallback(() => {
if(inputRef.current === null) return;
send('totp:confirm', {
clientId,
code: inputRef.current.value
})
}, [])
return <div>
<button onClick={() => setOpen(!open)}>TOTP</button>
{open && (
<div>
<img src={qr ?? undefined} />
<input ref={inputRef}></input>
<button onClick={submit}>CHECK</button>
</div>
)}
</div>
}

View File

@ -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 (
<ClientIdContext.Provider value={clientIdContextValue}>

View File

@ -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() {
<NameTextbox></NameTextbox><br></br>
<button onClick={() => setHomeServer(null)}>leave</button><br></br>
<LoginQR></LoginQR>
<Totp></Totp>
</div>
);
}

View File

@ -0,0 +1,2 @@
ALTER TABLE `clients`
ADD `totp` varchar(16) COLLATE 'utf8mb4_general_ci' NULL;

View File

@ -0,0 +1,3 @@
UPDATE clients
SET totp=?
WHERE uid=?;

View File

@ -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);

View File

@ -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
});
}
})

View File

@ -7,7 +7,7 @@
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"types" : ["node"],
"baseUrl": ".",