Support HashedPassword workflow for logins (#4469)

* Support HashedPassword workflow for logins

* Address comments in PR
This commit is contained in:
Zach H 2021-11-13 11:37:13 -05:00 committed by GitHub
parent 45d86e7ab7
commit 43eee6b32e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 93 additions and 7 deletions

View file

@ -6,6 +6,7 @@
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@material-ui/styles": "^4.11.4",
"crypto-js": "^4.1.1",
"dexie": "^3.0.3",
"jquery": "^3.4.1",
"lodash": "^4.17.15",

View file

@ -67,4 +67,8 @@ export interface ServerStateLogs {
export interface ServerStateSortUsersBy extends SortBy {
field: UserSortField
}
export interface RequestPasswordSaltParams {
user: string;
}

View file

@ -30,6 +30,13 @@ export const DefaultHosts: Host[] = [
localHost: 'server.cockatrice.us',
editable: false,
},
{
name: 'Rooster Beta',
host: 'beta.cockatrice.us/servatrice',
port: '4748',
localHost: 'beta.cockatrice.us',
editable: false,
},
{
name: 'Tetrarch',
host: 'mtg.tetrarch.co/servatrice',

View file

@ -121,6 +121,7 @@ const ProtoFiles = [
"response_join_room.proto",
"response_list_users.proto",
"response_login.proto",
"response_password_salt.proto",
"response_register.proto",
"response_replay_download.proto",
"response_replay_list.proto",

View file

@ -2,13 +2,14 @@ import {StatusEnum} from 'types';
import {RoomPersistence, SessionPersistence} from '../persistence';
import webClient from '../WebClient';
import {guid} from '../utils';
import {guid, hashPassword} from '../utils';
import {WebSocketConnectReason, WebSocketOptions} from "../services/WebSocketService";
import {
AccountActivationParams,
ForgotPasswordChallengeParams,
ForgotPasswordParams,
ForgotPasswordResetParams,
RequestPasswordSaltParams,
ServerRegisterParams
} from "../../store";
import NormalizeService from "../utils/NormalizeService";
@ -36,14 +37,19 @@ export class SessionCommands {
webClient.disconnect();
}
static login(): void {
const loginConfig = {
static login(passwordSalt?: string): void {
const loginConfig: any = {
...webClient.clientConfig,
userName: webClient.options.user,
password: webClient.options.pass,
clientid: guid()
};
if (passwordSalt) {
loginConfig.hashedPassword = hashPassword(passwordSalt, webClient.options.pass);
} else {
loginConfig.password = webClient.options.pass;
}
const CmdLogin = webClient.protobuf.controller.Command_Login.create(loginConfig);
const command = webClient.protobuf.controller.SessionCommand.create({
@ -110,6 +116,40 @@ export class SessionCommands {
});
}
static requestPasswordSalt(): void {
const options = webClient.options as unknown as RequestPasswordSaltParams;
const registerConfig = {
...webClient.clientConfig,
userName: options.user,
};
const CmdRequestPasswordSalt = webClient.protobuf.controller.Command_RequestPasswordSalt.create(registerConfig);
const sc = webClient.protobuf.controller.SessionCommand.create({
".Command_RequestPasswordSalt.ext" : CmdRequestPasswordSalt
});
webClient.protobuf.sendSessionCommand(sc, raw => {
switch (raw.responseCode) {
case webClient.protobuf.controller.Response.ResponseCode.RespOk:
const passwordSalt = raw[".Response_PasswordSalt.ext"].passwordSalt;
SessionCommands.login(passwordSalt);
break;
case webClient.protobuf.controller.Response.ResponseCode.RespRegistrationRequired:
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, "Login failed: incorrect username or password");
SessionCommands.disconnect();
break;
default:
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, "Login failed: Unknown Reason");
SessionCommands.disconnect();
break;
}
});
}
static register(): void {
const options = webClient.options as unknown as ServerRegisterParams;

View file

@ -113,7 +113,7 @@ function removeFromList({ listName, userName }: RemoveFromListData) {
}
function serverIdentification(info: ServerIdentificationData) {
const { serverName, serverVersion, protocolVersion } = info;
const { serverName, serverVersion, protocolVersion, serverOptions } = info;
if (protocolVersion !== webClient.protocolVersion) {
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`);
@ -124,7 +124,12 @@ function serverIdentification(info: ServerIdentificationData) {
switch (webClient.options.reason) {
case WebSocketConnectReason.LOGIN:
SessionCommands.updateStatus(StatusEnum.LOGGING_IN, 'Logging In...');
SessionCommands.login();
// Intentional use of Bitwise operator b/c of how Servatrice Enums work
if (serverOptions & webClient.protobuf.controller.Event_ServerIdentification.ServerOptions.SupportsPasswordHash) {
SessionCommands.requestPasswordSalt();
} else {
SessionCommands.login();
}
break;
case WebSocketConnectReason.REGISTER:
SessionCommands.register();
@ -198,6 +203,7 @@ export interface ServerIdentificationData {
protocolVersion: number;
serverName: string;
serverVersion: string;
serverOptions: number;
}
export interface ServerMessageData {

View file

@ -1,2 +1,3 @@
export * from "./guid.util";
export * from "./sanitizeHtml.util";
export * from "./sanitizeHtml.util";
export * from "./passwordHasher";

View file

@ -0,0 +1,26 @@
import sha512 from 'crypto-js/sha512';
import Base64 from 'crypto-js/enc-base64';
const HASH_ROUNDS = 1_000;
const SALT_LENGTH = 16;
export const hashPassword = (salt: string, password: string): string => {
let hashedPassword = salt + password;
for (let i = 0; i < HASH_ROUNDS; i++) {
// WHY DO WE DO IT THIS WAY?
hashedPassword = sha512(hashedPassword);
}
return salt + Base64.stringify(hashedPassword);
};
export const generateSalt = (): string => {
const characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
let salt = "";
for (let i = 0; i < SALT_LENGTH; i++) {
salt += characters.charAt(Math.floor(Math.random() * characters.length));
}
return salt;
}