totp in database
parent
6b496cc045
commit
a1784f5cb2
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ export {
|
|||
getClientId,
|
||||
setClientId,
|
||||
getHomeServer,
|
||||
setHomeServer,
|
||||
setHomeServer
|
||||
} from './settings';
|
||||
export {versions} from './versions';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE `clients`
|
||||
ADD `totp` varchar(16) COLLATE 'utf8mb4_general_ci' NULL;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
UPDATE clients
|
||||
SET totp=?
|
||||
WHERE uid=?;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types" : ["node"],
|
||||
|
||||
"baseUrl": ".",
|
||||
|
|
|
|||
Reference in New Issue