Client-side changes for cryptographic login

This commit is contained in:
Zhongheng Liu 2024-04-01 23:45:43 +03:00
commit 3fb94984ec
No known key found for this signature in database
5 changed files with 102 additions and 74 deletions

View file

@ -13,10 +13,6 @@ import strings from "../Intl/strings.json";
import { LangContext } from "../context"; import { LangContext } from "../context";
import { connectionAddress, endpoints } from "../consts"; import { connectionAddress, endpoints } from "../consts";
const Chat = ({ user }: { user: string }): React.ReactElement => { const Chat = ({ user }: { user: string }): React.ReactElement => {
const encryptMessage = (plaintext: string) => {
const ciphertext = plaintext;
return ciphertext;
}
const lang = useContext(LangContext); const lang = useContext(LangContext);
const chatPage = strings[lang].chat; const chatPage = strings[lang].chat;
const [messages, setMessages] = useState<ReactElement[]>([]); const [messages, setMessages] = useState<ReactElement[]>([]);
@ -82,7 +78,7 @@ const Chat = ({ user }: { user: string }): React.ReactElement => {
type: MessageType.MESSAGE, type: MessageType.MESSAGE,
fromUserId: user, fromUserId: user,
toUserId: "everyone", toUserId: "everyone",
content: encryptMessage(entryElement.value), content: entryElement.value,
timeMillis: Date.now(), timeMillis: Date.now(),
}; };
console.log( console.log(

View file

@ -1,10 +1,11 @@
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { contentTypes, domain, endpoints, port } from "../consts"; import { contentTypes, domain, endpoints, port } from "../consts";
import { LangContext, LoginType } from "../context"; import { LangContext, LoginType } from "../context";
import { User } from "../type/userTypes"; import { AuthData, User } from "../type/userTypes";
import "./Login.css"; import "./Login.css";
import strings from "../Intl/strings.json"; import strings from "../Intl/strings.json";
import { ECDH } from "crypto"; import { ECDH } from "crypto";
import { digestMessage } from "../crypto";
const encrypt = (rawPasswordString: string) => { const encrypt = (rawPasswordString: string) => {
// TODO Encryption method stub // TODO Encryption method stub
return rawPasswordString; return rawPasswordString;
@ -18,86 +19,94 @@ export const Login = ({
const [validText, setValidText] = useState<string | undefined>(); const [validText, setValidText] = useState<string | undefined>();
const lang = useContext(LangContext); const lang = useContext(LangContext);
const loginPage = strings[lang].login; const loginPage = strings[lang].login;
// TODO mk unit test
const registrationHandler = () => { const registrationHandler = () => {
const uname = (document.getElementById("username") as HTMLInputElement) const uname = (document.getElementById("username") as HTMLInputElement)
.value; .value;
const passwd = encrypt( digestMessage((document.getElementById("passwd") as HTMLInputElement).value).then((passwd) => {
(document.getElementById("passwd") as HTMLInputElement).value fetch(`https://${domain}:${port}${endpoints.register}`, {
); method: "POST",
fetch(`https://${domain}:${port}${endpoints.user}`, { mode: "cors",
method: "POST", headers: contentTypes.json,
mode: "cors", body: JSON.stringify({
headers: contentTypes.json, userName: uname,
body: JSON.stringify({ newUserPassword: passwd,
userName: uname, }),
dateJoined: Date.now(), })
passwordHash: passwd, .then((res) => res.json()).then((body) => {
}), const response = body as AuthData;
}).then((response) => { if (response.exists && !response.success) {throw new Error(loginPage.error.unameTakenOrInvalid)}
if (response.status === 400) { getProfile(uname).then((user) => {
// 400 Bad request setValid(true);
console.log("Username is taken or invalid!"); const futureDate = new Date();
setValid(false); futureDate.setHours(futureDate.getHours() + 2);
setValidText(loginPage.error.unameTakenOrInvalid); setLogin({
} else if (response.status === 200) { username: user.userName,
// 200 OK lastSeen: Date.now(),
const futureDate = new Date(); validUntil: futureDate.getUTCMilliseconds(),
futureDate.setHours(futureDate.getHours() + 2); });
setLogin({ document.title = `IRC User ${user.userName}`;
username: uname,
lastSeen: Date.now(),
validUntil: futureDate.getUTCMilliseconds(),
}); });
document.title = `IRC User ${uname}`; })
} .catch((error: Error) => {
setValid(false);
setValidText(error.message);
})
}); });
}; };
// TODO Make unit test
const getProfile = async(userName: string): Promise<User> => {
const res = await (await fetch(`https://${domain}:${port}${endpoints.user}?name=${userName}`,
{
method: "GET",
mode: "cors"
}
)).json()
return res;
}
// login button press handler // login button press handler
// TODO make unit test
const loginHandler = () => { const loginHandler = () => {
const uname = (document.getElementById("username") as HTMLInputElement) const uname = (document.getElementById("username") as HTMLInputElement)
.value; .value;
const passwd = encrypt( digestMessage(
(document.getElementById("passwd") as HTMLInputElement).value (document.getElementById("passwd") as HTMLInputElement).value
); ).then((passwd) => {
// async invocation of Fetch API // async invocation of Fetch API
fetch(`https://${domain}:${port}${endpoints.user}?name=${uname}`, { fetch(`https://${domain}:${port}${endpoints.auth}`, {
method: "GET", method: "POST",
mode: "cors", mode: "cors",
}) headers: {"Content-Type": "application/json"},
.then((res) => { body: JSON.stringify({
if (res.status === 404) { userName: uname,
console.log("404 not found encountered"); userPasswordHash: passwd,
throw new Error(loginPage.error.unameNotExists); })
} else if (res.status === 200) {
console.log("200 OK");
}
return res.json();
}) })
.then((userObject) => { .then(res => res.json())
if (!userObject) { .then((body) => {
return; const response = body as AuthData;
} if (!response.exists) {throw new Error(loginPage.error.unameNotExists)}
const user = userObject as User; else if (!response.success) {throw new Error(loginPage.error.passwdInvalid)}
const validLogin = passwd === user.passwordHash; else {
if (!validLogin) { getProfile(uname).then((user) => {
// login invalid // login valid
throw new Error(loginPage.error.passwdInvalid); setValid(true);
} else { const validUntilDate: Date = new Date();
// login valid validUntilDate.setHours(validUntilDate.getHours() + 2);
setValid(true); setLogin({
const validUntilDate: Date = new Date(); username: user.userName,
validUntilDate.setHours(validUntilDate.getHours() + 2); lastSeen: user.lastSeen,
setLogin({ validUntil: validUntilDate.getUTCMilliseconds(),
username: user.userName, });
lastSeen: user.lastSeen, document.title = `IRC User ${uname}`;
validUntil: validUntilDate.getUTCMilliseconds(), });
}); }
document.title = `IRC User ${uname}`; })
} .catch((reason: Error) => {
}) setValid(false);
.catch((reason: Error) => { setValidText(reason.message);
setValid(false); });
setValidText(reason.message);
}); });
}; };
return ( return (

View file

@ -7,6 +7,7 @@ export const endpoints = {
history: "/api/v1/msg/", history: "/api/v1/msg/",
user: "/api/v1/user", user: "/api/v1/user",
auth: "/api/v1/auth", auth: "/api/v1/auth",
register: "/api/v1/register",
oauth2: "", oauth2: "",
}; };
export const contentTypes = { export const contentTypes = {

15
src/crypto.ts Normal file
View file

@ -0,0 +1,15 @@
export const ENCRYPTION_TYPE = "SHA-256";
// Implemented according to https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
export const digestMessage = async (plaintext: string) => {
const textEncoder = new TextEncoder();
const digestArray = Array.from(
new Uint8Array(
await window.crypto.subtle.digest(
ENCRYPTION_TYPE,
textEncoder.encode(plaintext)
)
)
)
return digestArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
}

View file

@ -11,3 +11,10 @@ export type User = {
passwordHash: string; passwordHash: string;
// avatar: UserAvatar; // avatar: UserAvatar;
}; };
export type AuthData = {
success: boolean;
hasProfile: boolean;
exists: boolean;
authMessage: string;
authResponseTimestampMillis: number;
}