mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-06-19 05:13:54 -07:00
Support HashedPassword workflow for logins (#4469)
* Support HashedPassword workflow for logins * Address comments in PR
This commit is contained in:
parent
45d86e7ab7
commit
43eee6b32e
8 changed files with 93 additions and 7 deletions
|
|
@ -6,6 +6,7 @@
|
||||||
"@material-ui/core": "^4.11.4",
|
"@material-ui/core": "^4.11.4",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/styles": "^4.11.4",
|
"@material-ui/styles": "^4.11.4",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
"dexie": "^3.0.3",
|
"dexie": "^3.0.3",
|
||||||
"jquery": "^3.4.1",
|
"jquery": "^3.4.1",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
|
|
|
||||||
|
|
@ -67,4 +67,8 @@ export interface ServerStateLogs {
|
||||||
|
|
||||||
export interface ServerStateSortUsersBy extends SortBy {
|
export interface ServerStateSortUsersBy extends SortBy {
|
||||||
field: UserSortField
|
field: UserSortField
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestPasswordSaltParams {
|
||||||
|
user: string;
|
||||||
}
|
}
|
||||||
|
|
@ -30,6 +30,13 @@ export const DefaultHosts: Host[] = [
|
||||||
localHost: 'server.cockatrice.us',
|
localHost: 'server.cockatrice.us',
|
||||||
editable: false,
|
editable: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Rooster Beta',
|
||||||
|
host: 'beta.cockatrice.us/servatrice',
|
||||||
|
port: '4748',
|
||||||
|
localHost: 'beta.cockatrice.us',
|
||||||
|
editable: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Tetrarch',
|
name: 'Tetrarch',
|
||||||
host: 'mtg.tetrarch.co/servatrice',
|
host: 'mtg.tetrarch.co/servatrice',
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ const ProtoFiles = [
|
||||||
"response_join_room.proto",
|
"response_join_room.proto",
|
||||||
"response_list_users.proto",
|
"response_list_users.proto",
|
||||||
"response_login.proto",
|
"response_login.proto",
|
||||||
|
"response_password_salt.proto",
|
||||||
"response_register.proto",
|
"response_register.proto",
|
||||||
"response_replay_download.proto",
|
"response_replay_download.proto",
|
||||||
"response_replay_list.proto",
|
"response_replay_list.proto",
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@ import {StatusEnum} from 'types';
|
||||||
|
|
||||||
import {RoomPersistence, SessionPersistence} from '../persistence';
|
import {RoomPersistence, SessionPersistence} from '../persistence';
|
||||||
import webClient from '../WebClient';
|
import webClient from '../WebClient';
|
||||||
import {guid} from '../utils';
|
import {guid, hashPassword} from '../utils';
|
||||||
import {WebSocketConnectReason, WebSocketOptions} from "../services/WebSocketService";
|
import {WebSocketConnectReason, WebSocketOptions} from "../services/WebSocketService";
|
||||||
import {
|
import {
|
||||||
AccountActivationParams,
|
AccountActivationParams,
|
||||||
ForgotPasswordChallengeParams,
|
ForgotPasswordChallengeParams,
|
||||||
ForgotPasswordParams,
|
ForgotPasswordParams,
|
||||||
ForgotPasswordResetParams,
|
ForgotPasswordResetParams,
|
||||||
|
RequestPasswordSaltParams,
|
||||||
ServerRegisterParams
|
ServerRegisterParams
|
||||||
} from "../../store";
|
} from "../../store";
|
||||||
import NormalizeService from "../utils/NormalizeService";
|
import NormalizeService from "../utils/NormalizeService";
|
||||||
|
|
@ -36,14 +37,19 @@ export class SessionCommands {
|
||||||
webClient.disconnect();
|
webClient.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
static login(): void {
|
static login(passwordSalt?: string): void {
|
||||||
const loginConfig = {
|
const loginConfig: any = {
|
||||||
...webClient.clientConfig,
|
...webClient.clientConfig,
|
||||||
userName: webClient.options.user,
|
userName: webClient.options.user,
|
||||||
password: webClient.options.pass,
|
|
||||||
clientid: guid()
|
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 CmdLogin = webClient.protobuf.controller.Command_Login.create(loginConfig);
|
||||||
|
|
||||||
const command = webClient.protobuf.controller.SessionCommand.create({
|
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 {
|
static register(): void {
|
||||||
const options = webClient.options as unknown as ServerRegisterParams;
|
const options = webClient.options as unknown as ServerRegisterParams;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ function removeFromList({ listName, userName }: RemoveFromListData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function serverIdentification(info: ServerIdentificationData) {
|
function serverIdentification(info: ServerIdentificationData) {
|
||||||
const { serverName, serverVersion, protocolVersion } = info;
|
const { serverName, serverVersion, protocolVersion, serverOptions } = info;
|
||||||
|
|
||||||
if (protocolVersion !== webClient.protocolVersion) {
|
if (protocolVersion !== webClient.protocolVersion) {
|
||||||
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`);
|
SessionCommands.updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`);
|
||||||
|
|
@ -124,7 +124,12 @@ function serverIdentification(info: ServerIdentificationData) {
|
||||||
switch (webClient.options.reason) {
|
switch (webClient.options.reason) {
|
||||||
case WebSocketConnectReason.LOGIN:
|
case WebSocketConnectReason.LOGIN:
|
||||||
SessionCommands.updateStatus(StatusEnum.LOGGING_IN, 'Logging In...');
|
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;
|
break;
|
||||||
case WebSocketConnectReason.REGISTER:
|
case WebSocketConnectReason.REGISTER:
|
||||||
SessionCommands.register();
|
SessionCommands.register();
|
||||||
|
|
@ -198,6 +203,7 @@ export interface ServerIdentificationData {
|
||||||
protocolVersion: number;
|
protocolVersion: number;
|
||||||
serverName: string;
|
serverName: string;
|
||||||
serverVersion: string;
|
serverVersion: string;
|
||||||
|
serverOptions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerMessageData {
|
export interface ServerMessageData {
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export * from "./guid.util";
|
export * from "./guid.util";
|
||||||
export * from "./sanitizeHtml.util";
|
export * from "./sanitizeHtml.util";
|
||||||
|
export * from "./passwordHasher";
|
||||||
26
webclient/src/websocket/utils/passwordHasher.ts
Normal file
26
webclient/src/websocket/utils/passwordHasher.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue