Client-side changes for cryptographic login
This commit is contained in:
parent
3ceee1db4f
commit
3fb94984ec
5 changed files with 102 additions and 74 deletions
|
@ -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(
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
15
src/crypto.ts
Normal 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("");
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue