diff --git a/webclient/package.json b/webclient/package.json index d2eada7bc..d2249fb50 100644 --- a/webclient/package.json +++ b/webclient/package.json @@ -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", diff --git a/webclient/src/store/server/server.interfaces.ts b/webclient/src/store/server/server.interfaces.ts index b08c6f579..fd6e04ba2 100644 --- a/webclient/src/store/server/server.interfaces.ts +++ b/webclient/src/store/server/server.interfaces.ts @@ -67,4 +67,8 @@ export interface ServerStateLogs { export interface ServerStateSortUsersBy extends SortBy { field: UserSortField +} + +export interface RequestPasswordSaltParams { + user: string; } \ No newline at end of file diff --git a/webclient/src/types/server.tsx b/webclient/src/types/server.tsx index 4b4adce64..220928516 100644 --- a/webclient/src/types/server.tsx +++ b/webclient/src/types/server.tsx @@ -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', diff --git a/webclient/src/websocket/ProtoFiles.ts b/webclient/src/websocket/ProtoFiles.ts index 5d711cb74..d89fb21f9 100644 --- a/webclient/src/websocket/ProtoFiles.ts +++ b/webclient/src/websocket/ProtoFiles.ts @@ -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", diff --git a/webclient/src/websocket/commands/SessionCommands.ts b/webclient/src/websocket/commands/SessionCommands.ts index 1d2550729..d6b4b52db 100644 --- a/webclient/src/websocket/commands/SessionCommands.ts +++ b/webclient/src/websocket/commands/SessionCommands.ts @@ -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; diff --git a/webclient/src/websocket/events/SessionEvents.ts b/webclient/src/websocket/events/SessionEvents.ts index 8e4cbc227..e4f0bf824 100644 --- a/webclient/src/websocket/events/SessionEvents.ts +++ b/webclient/src/websocket/events/SessionEvents.ts @@ -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 { diff --git a/webclient/src/websocket/utils/index.ts b/webclient/src/websocket/utils/index.ts index 97ef6165b..135c4565b 100644 --- a/webclient/src/websocket/utils/index.ts +++ b/webclient/src/websocket/utils/index.ts @@ -1,2 +1,3 @@ export * from "./guid.util"; -export * from "./sanitizeHtml.util"; \ No newline at end of file +export * from "./sanitizeHtml.util"; +export * from "./passwordHasher"; \ No newline at end of file diff --git a/webclient/src/websocket/utils/passwordHasher.ts b/webclient/src/websocket/utils/passwordHasher.ts new file mode 100644 index 000000000..e7c396cee --- /dev/null +++ b/webclient/src/websocket/utils/passwordHasher.ts @@ -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; +} \ No newline at end of file