Added rcon password hashing to prevent them from being sent as plain-text.
Commands+results are still plain-text so ideally use dtls/tls/wss/ssh/https for rcon instead, but this should be compatible with ezquake+mvdsv. git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@5295 fc73d0e0-1445-4013-8a0c-d673dee63da5
This commit is contained in:
parent
f80c769ebe
commit
5886f59d76
2 changed files with 127 additions and 7 deletions
|
@ -46,6 +46,7 @@ int startuppending;
|
||||||
void Host_FinishLoading(void);
|
void Host_FinishLoading(void);
|
||||||
|
|
||||||
|
|
||||||
|
cvar_t cl_crypt_rcon = CVARFD("cl_crypt_rcon", "1", CVAR_ARCHIVE, "Controls whether to send a hash instead of sending rcon passwords as plain-text. Set to 1 for security, or 0 for backwards compatibility.\nYour command and any responses will still be sent as plain text.\nInstead, it is recommended to use rcon ONLY via dtls/tls/wss connections.");
|
||||||
cvar_t rcon_password = CVARF("rcon_password", "", CVAR_NOUNSAFEEXPAND);
|
cvar_t rcon_password = CVARF("rcon_password", "", CVAR_NOUNSAFEEXPAND);
|
||||||
|
|
||||||
cvar_t rcon_address = CVARF("rcon_address", "", CVAR_NOUNSAFEEXPAND);
|
cvar_t rcon_address = CVARF("rcon_address", "", CVAR_NOUNSAFEEXPAND);
|
||||||
|
@ -147,6 +148,9 @@ cvar_t noaim = CVARF("noaim", "", CVAR_ARCHIVE | CVAR_USERINFO);
|
||||||
cvar_t msg = CVARFD("msg", "1", CVAR_ARCHIVE | CVAR_USERINFO, "Filter console prints/messages. Only functions on QuakeWorld servers. 0=pickup messages. 1=death messages. 2=critical messages. 3=chat.");
|
cvar_t msg = CVARFD("msg", "1", CVAR_ARCHIVE | CVAR_USERINFO, "Filter console prints/messages. Only functions on QuakeWorld servers. 0=pickup messages. 1=death messages. 2=critical messages. 3=chat.");
|
||||||
cvar_t b_switch = CVARF("b_switch", "", CVAR_ARCHIVE | CVAR_USERINFO);
|
cvar_t b_switch = CVARF("b_switch", "", CVAR_ARCHIVE | CVAR_USERINFO);
|
||||||
cvar_t w_switch = CVARF("w_switch", "", CVAR_ARCHIVE | CVAR_USERINFO);
|
cvar_t w_switch = CVARF("w_switch", "", CVAR_ARCHIVE | CVAR_USERINFO);
|
||||||
|
#ifdef HEXEN2
|
||||||
|
cvar_t cl_playerclass=CVARF("cl_playerclass","", CVAR_ARCHIVE | CVAR_USERINFO);
|
||||||
|
#endif
|
||||||
cvar_t cl_nofake = CVARD("cl_nofake", "2", "value 0: permits \\r chars in chat messages\nvalue 1: blocks all \\r chars\nvalue 2: allows \\r chars, but only from teammates");
|
cvar_t cl_nofake = CVARD("cl_nofake", "2", "value 0: permits \\r chars in chat messages\nvalue 1: blocks all \\r chars\nvalue 2: allows \\r chars, but only from teammates");
|
||||||
cvar_t cl_chatsound = CVAR("cl_chatsound","1");
|
cvar_t cl_chatsound = CVAR("cl_chatsound","1");
|
||||||
cvar_t cl_enemychatsound = CVAR("cl_enemychatsound", "misc/talk.wav");
|
cvar_t cl_enemychatsound = CVAR("cl_enemychatsound", "misc/talk.wav");
|
||||||
|
@ -1437,7 +1441,49 @@ void CL_Rcon_f (void)
|
||||||
message[4] = 0;
|
message[4] = 0;
|
||||||
|
|
||||||
Q_strncatz (message, "rcon ", sizeof(message));
|
Q_strncatz (message, "rcon ", sizeof(message));
|
||||||
Q_strncatz (message, password, sizeof(message));
|
if (cl_crypt_rcon.ival)
|
||||||
|
{
|
||||||
|
char cryptpass[1024], crypttime[64];
|
||||||
|
const char *hex = "0123456789ABCDEF"; //must be upper-case for compat with mvdsv.
|
||||||
|
time_t clienttime;
|
||||||
|
time(&clienttime);
|
||||||
|
size_t digestsize;
|
||||||
|
unsigned char digest[64];
|
||||||
|
const unsigned char **tokens = alloca(sizeof(*tokens)*(4+Cmd_Argc()*2));
|
||||||
|
size_t *tokensizes = alloca(sizeof(*tokensizes)*(4+Cmd_Argc()*2));
|
||||||
|
int j, k;
|
||||||
|
|
||||||
|
for (j = 0; j < sizeof(time_t); j++)
|
||||||
|
{ //little-endian byte order, but big-endian nibble order. just screwed. for compat with ezquake.
|
||||||
|
crypttime[j*2+0] = hex[(clienttime>>(j*8+4))&0xf];
|
||||||
|
crypttime[j*2+1] = hex[(clienttime>>(j*8))&0xf];
|
||||||
|
}
|
||||||
|
crypttime[j*2] = 0;
|
||||||
|
|
||||||
|
tokens[0] = "rcon ";
|
||||||
|
tokens[1] = password;
|
||||||
|
tokens[2] = crypttime;
|
||||||
|
tokens[3] = " ";
|
||||||
|
for (j=0 ; j<Cmd_Argc()-i ; j++)
|
||||||
|
{
|
||||||
|
tokens[4+j*2+0] = Cmd_Argv(i+j);
|
||||||
|
tokens[4+j*2+1] = " ";
|
||||||
|
}
|
||||||
|
for (k = 0; k < 4+j*2; k++)
|
||||||
|
tokensizes[k] = strlen(tokens[k]);
|
||||||
|
digestsize = SHA1_m(digest, sizeof(digest), k, tokens, tokensizes);
|
||||||
|
|
||||||
|
for (j = 0; j < digestsize; j++)
|
||||||
|
{
|
||||||
|
cryptpass[j*2+0] = hex[digest[j]>>4];
|
||||||
|
cryptpass[j*2+1] = hex[digest[j]&0xf];
|
||||||
|
}
|
||||||
|
cryptpass[j*2] = 0;
|
||||||
|
Q_strncatz (message, cryptpass, sizeof(message));
|
||||||
|
Q_strncatz (message, crypttime, sizeof(message));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Q_strncatz (message, password, sizeof(message));
|
||||||
Q_strncatz (message, " ", sizeof(message));
|
Q_strncatz (message, " ", sizeof(message));
|
||||||
|
|
||||||
for ( ; i<Cmd_Argc() ; i++)
|
for ( ; i<Cmd_Argc() ; i++)
|
||||||
|
@ -4242,6 +4288,7 @@ void CL_Init (void)
|
||||||
Cvar_Register (&m_forward, cl_inputgroup);
|
Cvar_Register (&m_forward, cl_inputgroup);
|
||||||
Cvar_Register (&m_side, cl_inputgroup);
|
Cvar_Register (&m_side, cl_inputgroup);
|
||||||
|
|
||||||
|
Cvar_Register (&cl_crypt_rcon, cl_controlgroup);
|
||||||
Cvar_Register (&rcon_password, cl_controlgroup);
|
Cvar_Register (&rcon_password, cl_controlgroup);
|
||||||
Cvar_Register (&rcon_address, cl_controlgroup);
|
Cvar_Register (&rcon_address, cl_controlgroup);
|
||||||
|
|
||||||
|
@ -4309,6 +4356,9 @@ void CL_Init (void)
|
||||||
Cvar_Register (&noaim, cl_controlgroup);
|
Cvar_Register (&noaim, cl_controlgroup);
|
||||||
Cvar_Register (&b_switch, cl_controlgroup);
|
Cvar_Register (&b_switch, cl_controlgroup);
|
||||||
Cvar_Register (&w_switch, cl_controlgroup);
|
Cvar_Register (&w_switch, cl_controlgroup);
|
||||||
|
#ifdef HEXEN2
|
||||||
|
Cvar_Register (&cl_playerclass, cl_controlgroup);
|
||||||
|
#endif
|
||||||
|
|
||||||
Cvar_Register (&cl_demoreel, cl_controlgroup);
|
Cvar_Register (&cl_demoreel, cl_controlgroup);
|
||||||
Cvar_Register (&record_flush, cl_controlgroup);
|
Cvar_Register (&record_flush, cl_controlgroup);
|
||||||
|
|
|
@ -60,7 +60,9 @@ cvar_t fraglog_details = CVARD("fraglog_details", "1", "Bitmask\n1: killer+ki
|
||||||
|
|
||||||
cvar_t timeout = CVAR("timeout","65"); // seconds without any message
|
cvar_t timeout = CVAR("timeout","65"); // seconds without any message
|
||||||
cvar_t zombietime = CVAR("zombietime", "2"); // seconds to sink messages
|
cvar_t zombietime = CVAR("zombietime", "2"); // seconds to sink messages
|
||||||
// after disconnect
|
|
||||||
|
cvar_t sv_crypt_rcon = CVARFD("sv_crypt_rcon", "", CVAR_ARCHIVE, "Controls whether the rcon password must be hashed or not. Hashed passwords also partially prevent replay attacks, but does NOT prevent malicious actors from reading the commands/results.\n0: completely insecure. ONLY allows plain-text passwords. Do not use.\n1: Mandatory hashing (recommended).\nEmpty: Allow either, whether the password is secure or not is purely the client's responsibility/fault. Only use this for comptibility with old clients.");
|
||||||
|
cvar_t sv_crypt_rcon_clockskew = CVARFD("sv_timestamplen", "60", CVAR_ARCHIVE, "Limits clock skew to reduce (delayed) replay attacks");
|
||||||
#ifdef SERVERONLY
|
#ifdef SERVERONLY
|
||||||
cvar_t developer = CVAR("developer","0"); // show extra messages
|
cvar_t developer = CVAR("developer","0"); // show extra messages
|
||||||
|
|
||||||
|
@ -3328,16 +3330,83 @@ client_t *SVC_DirectConnect(void)
|
||||||
return newcl;
|
return newcl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int dehex(int i)
|
||||||
|
{
|
||||||
|
if (i >= '0' && i <= '9')
|
||||||
|
return (i-'0');
|
||||||
|
else if (i >= 'A' && i <= 'F')
|
||||||
|
return (i-'A'+10);
|
||||||
|
else
|
||||||
|
return (i-'a'+10);
|
||||||
|
}
|
||||||
int Rcon_Validate (void)
|
int Rcon_Validate (void)
|
||||||
{
|
{
|
||||||
if (!strlen (rcon_password.string))
|
const char *realpass = rcon_password.string;
|
||||||
|
const char *pass = Cmd_Argv(1);
|
||||||
|
if (!strlen (realpass))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (strcmp (Cmd_Argv(1), rcon_password.string) )
|
if (!sv_crypt_rcon.ival)
|
||||||
return 0;
|
{ //vanilla-compatible
|
||||||
|
if (!strcmp (pass, realpass) )
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (sv_crypt_rcon.ival || !*sv_crypt_rcon.string)
|
||||||
|
{ //ezquake-compatible
|
||||||
|
//the password arg is "[SHA1Digest][unixtime-in-hex]" where the digest is "[arg0] password[unixtime] arg0 arg1 argn "
|
||||||
|
time_t clienttime = 0;
|
||||||
|
time_t servertime = 0;
|
||||||
|
intptr_t timediff;
|
||||||
|
qbyte b;
|
||||||
|
|
||||||
return 1;
|
const size_t digestsize = 20;
|
||||||
|
size_t i, k;
|
||||||
|
unsigned char digest[digestsize];
|
||||||
|
const unsigned char **tokens = alloca(sizeof(*tokens)*(Cmd_Argc()*2+5)); //overallocation in case argc is 0.
|
||||||
|
size_t *toksizes = alloca(sizeof(*toksizes)*(Cmd_Argc()*2+5)); //overallocation in case argc is 0.
|
||||||
|
if (strlen(pass) > digestsize*2)
|
||||||
|
{
|
||||||
|
for (i = 0; pass[digestsize*2+i] && i < sizeof(time_t)*2; i++)
|
||||||
|
{ //mixed endian for compat with ezquake
|
||||||
|
if (!(i & 1))
|
||||||
|
clienttime |= dehex(pass[digestsize*2+i]) << (((i/2)*8)+4);
|
||||||
|
else
|
||||||
|
clienttime |= dehex(pass[digestsize*2+i]) << (((i/2)*8)+0);
|
||||||
|
}
|
||||||
|
time(&servertime);
|
||||||
|
timediff = servertime-clienttime;
|
||||||
|
//make sure the client's time is within our allowed bounds, to prevent (extreme) replay attacks.
|
||||||
|
if (!sv_crypt_rcon_clockskew.value || (timediff >= -sv_crypt_rcon_clockskew.ival && timediff <= sv_crypt_rcon_clockskew.ival))
|
||||||
|
{
|
||||||
|
tokens[0] = Cmd_Argv(0);
|
||||||
|
tokens[1] = " ";
|
||||||
|
tokens[2] = realpass;
|
||||||
|
tokens[3] = pass+digestsize*2;
|
||||||
|
tokens[4] = " ";
|
||||||
|
for (i = 0; i < Cmd_Argc()-2; i++)
|
||||||
|
{
|
||||||
|
tokens[5+i*2+0] = Cmd_Argv(i+2);
|
||||||
|
tokens[5+i*2+1] = " "; //a trailing space is required.
|
||||||
|
}
|
||||||
|
for (k = 0; k < 5+i*2; k++)
|
||||||
|
toksizes[k] = strlen(tokens[k]);
|
||||||
|
if (digestsize > 0 && digestsize == SHA1_m(digest, sizeof(digest), k, tokens, toksizes))
|
||||||
|
{
|
||||||
|
for (i = 0;;i++)
|
||||||
|
{
|
||||||
|
if (i == digestsize)
|
||||||
|
return 1;
|
||||||
|
if (!pass[i*2+0] || !pass[i*2+1])
|
||||||
|
break; //premature termination
|
||||||
|
b = dehex(pass[i*2+0])*16+dehex(pass[i*2+1]);
|
||||||
|
if (b != digest[i])
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -5077,6 +5146,7 @@ void SV_InitLocal (void)
|
||||||
Log_Init();
|
Log_Init();
|
||||||
}
|
}
|
||||||
rcon_password.restriction = RESTRICT_MAX; //no cheatie rconers changing rcon passwords...
|
rcon_password.restriction = RESTRICT_MAX; //no cheatie rconers changing rcon passwords...
|
||||||
|
Cvar_Register (&sv_crypt_rcon, cvargroup_servercontrol);
|
||||||
Cvar_Register (&spectator_password, cvargroup_servercontrol);
|
Cvar_Register (&spectator_password, cvargroup_servercontrol);
|
||||||
|
|
||||||
Cvar_Register (&sv_mintic, cvargroup_servercontrol);
|
Cvar_Register (&sv_mintic, cvargroup_servercontrol);
|
||||||
|
|
Loading…
Reference in a new issue