main
Valerie 2022-08-03 20:01:51 -04:00
parent 98a1906860
commit f3c8a2e482
13 changed files with 303 additions and 127 deletions

View File

@ -8,7 +8,6 @@ export function connectApi(url: string) {
const connect = async () => {
try {
connectionAttempts ++;
// console.log('connecting to', url);
socket = new WebSocket(url);
} catch (e) {
if(destroy) return;
@ -25,6 +24,7 @@ export function connectApi(url: string) {
socket.addEventListener('open', () => {
if(socket === null) return;
connectionAttempts = 0;
console.log('connected to', url);
// socket.send('Hello Server!');
});

View File

@ -5,10 +5,12 @@ import { FaUserPlus } from 'react-icons/fa';
import ServerConnection from "../components/ServerConnection";
import useHomeServer from "../contexts/PersistentState/useHomeServerNative";
import { BigButton } from "./BigButton";
import { SignUp } from "./SignUp";
import { MdOutlineNavigateNext } from 'react-icons/md';
import useHover from "../hooks/useHover";
import { AiOutlineEdit } from "react-icons/ai";
import { useApi } from "../lib/useApi";
import useSessionToken from "../hooks/useSessionToken";
import useClientId from "../hooks/useClientId";
export default function NewAccount() {
@ -70,11 +72,8 @@ export default function NewAccount() {
// const homeServerInputRef = useRef<HTMLInputElement>(null);
const { setHomeServer, homeServer } = useHomeServer();
const [homeServerInput, setHomeServerInput] = useState<string>(homeServer ?? '');
const [usernameInput, setUsernameInput] = useState('');
const [authCodeInput, setAuthCodeInput] = useState('');
const [returning, setReturning] = useState(true);
const [connection, setConnection] = useState<WebSocket | null>(null);
const [connecting, setConnecting] = useState(false);
// const [connection, setConnection] = useState<WebSocket | null>(null);
const [connectionError, setConnectionError] = useState('');
const [edittingHomeServer, setEdittingHomeServer] = useState(false);
const [homeServerInputRef, homeServerHovered] = useHover<HTMLInputElement>();
@ -88,29 +87,43 @@ export default function NewAccount() {
}
}, [homeServer]);
const connect = useCallback((url: string) => {
const [connecting, setConnecting] = useState(false);
const [connectionSucceeded, setConnectionSucceeded] = useState(false);
const connect = useCallback(() => {
if(connecting) return;
setHomeServer(url);
const url = homeServerInput;
setConnecting(true);
const ws = new WebSocket(url);
try {
const ws = new WebSocket(url);
ws.addEventListener('open', () => {
setConnecting(false);
setConnectionSucceeded(true);
setHomeServer(homeServerInput);
setEdittingHomeServer(false);
});
ws.addEventListener('error', (e) => {
setConnecting(false);
setConnectionSucceeded(false);
setConnectionError('Connection failed')
});
} catch (e) {
setConnecting(false)
setConnectionSucceeded(false);
setConnectionError('Connection failed in catch');
}
}, [connecting, homeServerInput]);
ws.addEventListener('open', () => {
setConnecting(false);
setConnection(ws);
setConnectionError('');
});
ws.addEventListener('close', (e) => {
setConnecting(false);
setConnection(null);
console.log(e)
});
ws.addEventListener('error', (e) => {
setConnectionError('Connection failed')
});
}, [connecting]);
const next = useCallback(() => {
// debugger;
if(edittingHomeServer) {
connect()
} else {
console.log('do login');
}
}, [homeServer, homeServerInput, edittingHomeServer, connect])
// return (
// <div style={{
@ -181,7 +194,8 @@ export default function NewAccount() {
background: 'var(--neutral-3)',
}}>
<div style={{
width: '450px',
width: 'calc(100% - 40px)',
maxWidth: '450px',
background: 'var(--neutral-4)',
boxShadow: '0px 4px 20px 0px var(--neutral-1)',
borderRadius: '8px',
@ -254,57 +268,173 @@ export default function NewAccount() {
onClick={(e) => {
setEdittingHomeServer(true);
}}
onKeyPress={(e: any) => {
if(e.code === 'Enter') {
if(homeServer === homeServerInput)
return setEdittingHomeServer(false);
setHomeServer(homeServerInput)
}
}}
onKeyPress={(e: any) => e.code === 'Enter' && next()}
></Input>
<div style={{
paddingLeft: '16px'
}}>
{(connecting) ? (
<div style={{ color: 'var(--neutral-7)'}}>
Connecting...
</div>
) : (
(!connectionSucceeded) && (
<div style={{ color: 'var(--red)'}}>
{connectionError}
</div>
)
)}
</div>
</div>
<Label>Username</Label>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
}}>
<Input
disabled={edittingHomeServer}
value={usernameInput}
setValue={setUsernameInput}
></Input>
</div>
<Label>Auth Code</Label>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
}}>
<Input
disabled={edittingHomeServer}
value={authCodeInput}
setValue={setAuthCodeInput}
></Input>
</div>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
textAlign: 'right'
}}>
<BigButton
icon={MdOutlineNavigateNext}
text="Next"
selected={false}
angle={6}
width="auto"
inline={true}
onClick={() => {}}
></BigButton>
</div>
<ServerConnection url={homeServer ?? ''}>
{(returning) ? (
<Login disabled={edittingHomeServer}></Login>
) : (
<SignUp disabled={edittingHomeServer}></SignUp>
)}
</ServerConnection>
{edittingHomeServer && <Next onClick={next}></Next>}
</div>
</div>
)
}
function Next(props: {
onClick?: (e: any) => void
}) {
return (
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
textAlign: 'right'
}}>
<BigButton
icon={MdOutlineNavigateNext}
text="Next"
selected={false}
angle={6}
width="auto"
inline={true}
onClick={props.onClick}
></BigButton>
</div>
)
}
interface LoginProps {
disabled?: boolean
}
function Login(props: LoginProps) {
const [usernameInput, setUsernameInput] = useState('');
const [authCodeInput, setAuthCodeInput] = useState('');
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const { setSessionToken } = useSessionToken();
const { setClientId } = useClientId();
const { send } = useApi({
'session:login'({ err, sessionToken, clientId }) {
if(err) {
setSuccess(null);
setError(err);
return;
}
setError(null);
setSuccess('Success!');
setTimeout(() => {
setClientId(clientId);
setSessionToken(sessionToken);
}, 1000)
}
})
const next = () => {
send('session:login', {
username: usernameInput,
totp: authCodeInput
})
}
return (
<>
<Label>Username</Label>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
}}>
<Input
disabled={props.disabled}
value={usernameInput}
setValue={setUsernameInput}
focusOnEenable={true}
></Input>
</div>
<Label>Auth Code</Label>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
}}>
<Input
disabled={props.disabled}
value={authCodeInput}
setValue={setAuthCodeInput}
></Input>
</div>
{error && <div style={{ color: 'var(--red)', textAlign: 'center' }}>
{error}
</div>}
{success && <div style={{ color: 'var(--green)', textAlign: 'center' }}>
{success}
</div>}
{!props.disabled && <Next onClick={next}></Next>}
</>
)
}
interface SignUpProps {
disabled?: boolean
}
function SignUp(props: SignUpProps) {
const [usernameInput, setUsernameInput] = useState('');
const [authCodeInput, setAuthCodeInput] = useState('');
const next = () => {
}
return (
<>
<Label>Username</Label>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
}}>
<Input
disabled={props.disabled}
value={usernameInput}
setValue={setUsernameInput}
focusOnEenable={true}
></Input>
</div>
<Label>Auth Code</Label>
<div style={{
transform: 'skew(6deg, 0deg)',
margin: '8px',
}}>
<Input
disabled={props.disabled}
value={authCodeInput}
setValue={setAuthCodeInput}
></Input>
</div>
{!props.disabled && <Next onClick={next}></Next>}
</>
)
}
function Label(props: any) {
return <label style={{
paddingLeft: '24px',

View File

@ -0,0 +1 @@
SELECT name, username, uid FROM clients WHERE username=?

View File

@ -0,0 +1 @@
SELECT totp FROM clients WHERE uid=?;

View File

@ -19,7 +19,7 @@ export function expose(router: Function, port: number) {
}
const {action, data} = message;
try {
if(typeof data === 'object' && 'sessionToken' in data) {
if(typeof data === 'object' && 'sessionToken' in data && data.sessionToken !== null) {
const auth = await validateSessionToken(data.sessionToken);
data.$sessionToken = data.sessionToken;
delete data['sessionToken'];

View File

@ -0,0 +1,16 @@
import query from "../db/query";
import addSessionToken from '../db/snippets/session/new.sql';
import { rb32 } from "../lib/rb32";
export const generateSessionToken = async (clientId: string) => {
let token = '';
for (let i = 0; i < 64; i++) {
token += rb32();
}
console.log('created session token', clientId, token);
// scnd min hr day year
const year = 1000 * 60 * 60 * 24 * 365;
const expiration = Date.now() + year;
await query(addSessionToken, clientId, expiration, token);
return token;
};

View File

@ -0,0 +1,3 @@
import { rb32 } from "../lib/rb32";
export const generateTotpKey = () => rb32() + rb32();

View File

@ -0,0 +1,8 @@
import query from '../db/query'
import getByUsername from '../db/snippets/client/getByUsername.sql'
export async function getClientIdByUsername(username: string) {
const res = await query(getByUsername, username);
if(res === null || res.length !== 1) return null;
return res[0].uid;
}

View File

@ -0,0 +1,25 @@
import { randomBytes } from 'crypto';
// 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'[n] as string;
export 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);
}

View File

@ -0,0 +1,9 @@
import query from '../db/query'
import getTotpKey from '../db/snippets/client/getTotpKeyByClientId.sql'
import { validateTotp } from './validateTotp';
export async function validateClientTotp(clientId: string, code: string) {
const res = await query(getTotpKey, clientId);
if(res === null || res.length !== 1) return false;
return validateTotp(res[0].totp, code);
}

View File

@ -0,0 +1,9 @@
import getToken from 'totp-generator';
export const validateTotp = (key: string, code: string) => {
return [
getToken(key, { timestamp: Date.now() }),
getToken(key, { timestamp: Date.now() - 30 * 1000 }),
getToken(key, { timestamp: Date.now() - 2 * 30 * 1000 })
].includes(code);
};

View File

@ -4,6 +4,12 @@ import { reply } from "../lib/WebSocketServer"
import invalidate from '../db/snippets/session/invalidate.sql'
import _get from '../db/snippets/session/get.sql'
import query from "../db/query";
import { getClientIdByUsername } from "../lib/getClientIdByUsername";
import { generateSessionToken } from "../lib/generateSessionToken";
import { validateTotp } from "../lib/validateTotp";
import { validateClientTotp } from "../lib/validateClientTotp";
const randomWait = async () => await new Promise(res => setTimeout(res, Math.random() * 300));
export default router({
async 'invalidate'(data: any) {
@ -11,6 +17,25 @@ export default router({
return reply({
err: null
})
},
async 'login'(data: any) {
await randomWait();
const { username, totp } = data;
const clientId = await getClientIdByUsername(username);
if(clientId === null) return reply({
err: 'Incorrect username or auth code'
});
const validTotp = await validateClientTotp(clientId, totp);
console.log(username, clientId, validTotp);
if(!validTotp) return reply({
err: 'Incorrect username or auth code'
});
const sessionToken = await generateSessionToken(clientId);
return reply({
err: null,
sessionToken,
clientId
})
}
});

View File

@ -1,68 +1,17 @@
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';
import addSessionToken from '../db/snippets/session/new.sql';
const validateTotp = (key: string, code: string) => {
return [
getToken(key, { timestamp: Date.now() }),
getToken(key, { timestamp: Date.now() - 30 * 1000 }),
getToken(key, { timestamp: Date.now() - 2 * 30 * 1000})
].includes(code);
}
const generateSessionToken = async (clientId: string) => {
let token = '';
for(let i = 0; i < 64; i ++) {
token += rb32()
}
console.log('created session token', clientId, token);
// scnd min hr day year
const year = 1000 * 60 * 60 * 24 * 365;
const expiration = Date.now() + year;
await query(addSessionToken, clientId, expiration, token);
return token;
}
// 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'[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();
import { validateTotp } from "../lib/validateTotp";
import { generateSessionToken } from "../lib/generateSessionToken";
import { generateTotpKey } from "../lib/generateTotpKey";
const proposals: any = {}
export default router({
'propose'(clientId: string) {
if(clientId in proposals) return reply(proposals[clientId]);
const key = totpKey();
const key = generateTotpKey();
proposals[clientId] = key;
console.log(clientId, proposals)
setTimeout(() => {