diff --git a/README b/README index 2ecc7ef..623a231 100644 --- a/README +++ b/README @@ -1,3 +1,33 @@ +dgamelaunch + security +====================== + +This branch was modified to enable proper password salting, using the +openssl library and pbkdf2 password hashing function. For compatibility +purposes, you can still compile with only sqlite (which salts passwords +with themselves using an MD5 based algorithm, or with neither. + +--enable-pbkdf2 requires --enable-sqlite + +dgamelaunch *should* work with neither pbkdf2 nor sqlite, but I wasn't +able to get it to work with my Dockerfile, where these were tested. + +Tests were carried out on a Debian 8 based Docker container. + +In addition, strings that ever hold raw passwords from the user, are now +cleared out of memory as soon as unnecessary. This is done regardless of +which flags you compile with. + +If you have an sqlite3 database using the old password hashing method, +there's a new command you can include in dgamelaunch.conf to transfer +users over to the new database. + +update_passwd will prompt users to login, if they successfully +login using their old password, it will prompt them to change +their password. The resulting password will be salted and hashed +with pbkdf2. For this to work, you must merge the old database +with the new one, with the salt entry as an empty string for users +who still need to migrate their passwords over. + dgamelaunch =========== @@ -40,6 +70,7 @@ Some options you might want give to autogen: --with-config-file=/absolute/path/to/dgamelaunch.config --enable-shmem --enable-sqlite + --enable-pbkdf2 Dgamelaunch should compile without issue on Linux, Solaris, FreeBSD 4 and 5. diff --git a/config.l b/config.l index b3b78d4..3412b87 100644 --- a/config.l +++ b/config.l @@ -118,6 +118,7 @@ chpasswd { yylval.i = DGLCMD_CHPASSWD; return TYPE_DGLCMD0; } chmail { yylval.i = DGLCMD_CHMAIL; return TYPE_DGLCMD0; } watch_menu { yylval.i = DGLCMD_WATCH_MENU; return TYPE_DGLCMD0; } ask_login { yylval.i = DGLCMD_LOGIN; return TYPE_DGLCMD0; } +update_passwd { yylval.i = DGLCMD_UPDATEPW; return TYPE_DGLCMD0; } ask_register { yylval.i = DGLCMD_REGISTER; return TYPE_DGLCMD0; } quit { yylval.i = DGLCMD_QUIT; return TYPE_DGLCMD0; } play_game { yylval.i = DGLCMD_PLAYGAME; return TYPE_DGLCMD1; } diff --git a/configure.ac b/configure.ac index 5b1d3bf..1efa00a 100644 --- a/configure.ac +++ b/configure.ac @@ -26,7 +26,7 @@ fi case "$(uname -s)" in Linux | *BSD) - MY_LIBS="$MY_LIBS -lutil -lcrypt" + MY_LIBS="$MY_LIBS -lutil" AC_DEFINE(NOSTREAMS, 1, [Don't use SVR4 streams support in ttyrec.]) ;; esac @@ -113,6 +113,24 @@ if test "$enable_sqlite" = yes; then fi +AC_ARG_ENABLE(pbkdf2, +[AC_HELP_STRING([--enable-pbkdf2], [Enable pbkdf2 password hashing. Requires sqlite])], +[], []) + +if test "$enable_pbkdf2" = yes; then + AC_MSG_RESULT([Enabling pbkdf2 password hashing.]) + AC_DEFINE(USE_PBKDF2,1,[Enable pbkdf2 password hashing.]) + AC_CHECK_HEADERS([openssl/rand.h], [], [AC_MSG_ERROR([openssl/rand.h not found.])], []) + AC_CHECK_HEADERS([openssl/evp.h], [], [AC_MSG_ERROR([openssl/evp.h not found.])], []) + MY_LIBS="$MY_LIBS -lcrypto -lcrypt" + + if test "$enable_sqlite" = no; then + AC_MSG_ERROR([sqlite must be enabled for pbkdf2 password hashing.]) + fi + + else + MY_LIBS="$MY_LIBS -lcrypt" +fi dgl_rlimit_core_default=157286400 diff --git a/dgamelaunch.c b/dgamelaunch.c index fc831a9..4d7596f 100644 --- a/dgamelaunch.c +++ b/dgamelaunch.c @@ -93,6 +93,12 @@ #include #include +/* crypto stuff */ +#ifdef USE_PBKDF2 +# include +# include +#endif + extern FILE* yyin; extern int yyparse (); @@ -157,6 +163,9 @@ cpy_me(struct dg_user *me) if (me->email) tmp->email = strdup(me->email); if (me->env) tmp->env = strdup(me->env); if (me->password) tmp->password = strdup(me->password); +#ifdef USE_PBKDF2 + if (me->salt) tmp->salt = strdup(me->salt); +#endif tmp->flags = me->flags; } return tmp; @@ -1485,6 +1494,155 @@ change_email () } } + +#ifdef USE_PBKDF2 +int +changepw (int dowrite) +{ + int error = 2; + + /* A precondition is that struct `me' exists because we can be not-yet-logged-in. */ + if (!me) { + debug_write("no 'me' in changepw"); + graceful_exit (122); /* Die. */ + } + + if (me->flags & DGLACCT_PASSWD_LOCK) { + clear(); + drawbanner(&banner); + mvprintw(5, 1, "Sorry, you cannot change the password.--More--"); + dgl_getch(); + return 0; + } + + char *buf = (char *)malloc((DGL_PASSWDLEN+1) * sizeof(char)); + char *repeatbuf = (char *)malloc((DGL_PASSWDLEN+1) * sizeof(char)); + + while (error) + { + clear (); + + drawbanner (&banner); + + mvprintw (5, 1, + "Please enter a%s password. Remember that this is sent over the net", + loggedin ? " new" : ""); + mvaddstr (6, 1, + "in plaintext, so make it something new and expect it to be relatively"); + mvaddstr (7, 1, "insecure."); + mvprintw (8, 1, + "%i character max. No ':' characters. Blank line to abort.", DGL_PASSWDLEN); + mvaddstr (10, 1, "=> "); + + if (error == 1) + { + mvaddstr (15, 1, "Sorry, the passwords don't match. Try again."); + move (10, 4); + } + + refresh (); + + if (mygetnstr (buf, DGL_PASSWDLEN, 0) != OK) + { + memset_s(buf, 0, strlen(buf)); + memset_s(repeatbuf, 0, strlen(repeatbuf)); + free(buf); + free(repeatbuf); + return 0; + } + + if (*buf == '\0') + { + memset_s(buf, 0, strlen(buf)); + memset_s(repeatbuf, 0, strlen(repeatbuf)); + free(buf); + free(repeatbuf); + return 0; + } + + if (strchr (buf, ':') != NULL) { + debug_write("cannot have ':' in passwd"); + memset_s(buf, 0, strlen(buf)); + memset_s(repeatbuf, 0, strlen(repeatbuf)); + + free(buf); + free(repeatbuf); + graceful_exit (112); + } + + mvaddstr (12, 1, "And again:"); + mvaddstr (13, 1, "=> "); + + if (mygetnstr (repeatbuf, DGL_PASSWDLEN, 0) != OK) + { + memset_s(buf, 0, strlen(buf)); + memset_s(repeatbuf, 0, strlen(repeatbuf)); + free(buf); + free(repeatbuf); + return 0; + } + + if (!strcmp (buf, repeatbuf)) + { + error = 0; + memset_s(repeatbuf, 0, strlen(repeatbuf)); + free(repeatbuf); + } + else + error = 1; + } + + /* Generate salt */ + unsigned char salt[DGL_SALTLEN]; + unsigned char dk[DGL_KEYLEN]; + + char asalt[2*DGL_SALTLEN+1]; + char adk[2*DGL_KEYLEN+1]; + + if( !RAND_bytes(salt,DGL_SALTLEN) ){ + memset_s(buf, 0, strlen(buf)); + free(buf); + return 0; + } + + if(!PKCS5_PBKDF2_HMAC_SHA1(buf, strlen(buf), salt, DGL_SALTLEN, DGL_ITERATION, DGL_KEYLEN, dk)){ + memset_s(buf, 0, strlen(buf)); + free(buf); + return 0; + } + + clear (); + drawbanner (&banner); + + if(!hex_to_ascii(salt, asalt, DGL_SALTLEN)) + { + return 0; + } + + if(!hex_to_ascii(dk, adk, DGL_KEYLEN)) + { + return 0; + } + + memset_s(buf, 0, strlen(buf)); + free(buf); + + + free(me->salt); + free(me->password); + + me->salt = strdup(asalt); + me->password = strdup(adk); + + + if (dowrite) + writefile (0); + + return 1; +} + + +#else int changepw (int dowrite) { @@ -1531,13 +1689,13 @@ changepw (int dowrite) refresh (); if (mygetnstr (buf, DGL_PASSWDLEN, 0) != OK) - return 0; + return 0; if (*buf == '\0') return 0; if (strchr (buf, ':') != NULL) { - debug_write("cannot have ':' in passwd"); + debug_write("cannot have ':' in passwd"); graceful_exit (112); } @@ -1545,7 +1703,7 @@ changepw (int dowrite) mvaddstr (13, 1, "=> "); if (mygetnstr (repeatbuf, DGL_PASSWDLEN, 0) != OK) - return 0; + return 0; if (!strcmp (buf, repeatbuf)) error = 0; @@ -1562,6 +1720,12 @@ changepw (int dowrite) return 1; } +#endif + + + + + /* ************************************************************* */ void @@ -1793,6 +1957,7 @@ initcurses () void autologin (char* user, char *pass) { + struct dg_user *tmp; tmp = userexist(user, 0); if (tmp) { @@ -1808,7 +1973,9 @@ autologin (char* user, char *pass) void loginprompt (int from_ttyplay) { - char user_buf[DGL_PLAYERNAMELEN+1], pw_buf[DGL_PASSWDLEN+2]; + char user_buf[DGL_PLAYERNAMELEN+1]; + char* pw_buf; + int error = 2; loggedin = 0; @@ -1855,22 +2022,32 @@ loginprompt (int from_ttyplay) drawbanner (&banner); + pw_buf = (char*)malloc((DGL_PASSWDLEN+2)* sizeof(char)); + mvaddstr (5, 1, "Please enter your password."); mvaddstr (7, 1, "=> "); refresh (); - if (mygetnstr (pw_buf, DGL_PASSWDLEN, 0) != OK) + if (mygetnstr (pw_buf, DGL_PASSWDLEN, 0) != OK){ + memset_s(pw_buf, 0, strlen(pw_buf)); + free(pw_buf); + me = NULL; return; + } if (passwordgood (pw_buf)) { - if (me->flags & DGLACCT_LOGIN_LOCK) { + memset_s(pw_buf, 0, strlen(pw_buf)); + free(pw_buf); + + if (me->flags & DGLACCT_LOGIN_LOCK) { + me = NULL; clear (); mvprintw(5, 1, "Sorry, that account has been banned.--More--"); dgl_getch(); return; - } + } loggedin = 1; if (from_ttyplay) @@ -1881,6 +2058,8 @@ loginprompt (int from_ttyplay) } else { + memset_s(pw_buf, 0, strlen(pw_buf)); + free(pw_buf); me = NULL; if (from_ttyplay == 1) { @@ -1965,9 +2144,7 @@ newuser () error = 1; } - if (strlen (buf) < 2) - error = 1; - + if (strlen (buf) < 2) error = 1; if (strlen (buf) == 0) { free(me); @@ -2048,6 +2225,33 @@ newuser () /* ************************************************************* */ +/* crypto changes */ +#ifdef USE_PBKDF2 +int +passwordgood (char *cpw) +{ + assert (me != NULL); + + unsigned char testdk[DGL_KEYLEN+1]; + unsigned char dk[DGL_KEYLEN+1]; + unsigned char salt[DGL_SALTLEN+1]; + + ascii_to_hex(me->salt, salt, DGL_SALTLEN); + ascii_to_hex(me->password, dk, DGL_KEYLEN); + + if(!PKCS5_PBKDF2_HMAC_SHA1(cpw, strlen(cpw), salt, DGL_SALTLEN, DGL_ITERATION, DGL_KEYLEN, testdk)){ + /* proper error handling needed */ + return 0; + } + + if(!strncmp(dk,testdk,DGL_KEYLEN)){ + return 1; + } + + return 0; +} + +#else int passwordgood (char *cpw) { @@ -2057,14 +2261,22 @@ passwordgood (char *cpw) crypted = crypt (cpw, cpw); if (crypted == NULL) return 0; + +# ifdef USE_SQLITE3 if (!strncmp (crypted, me->password, DGL_PASSWDLEN)) return 1; + +# else if (!strncmp (cpw, me->password, DGL_PASSWDLEN)) return 1; +# endif + return 0; } +#endif + /* ************************************************************* */ int @@ -2238,6 +2450,10 @@ userexist_callback(void *NotUsed, int argc, char **argv, char **colname) userexist_tmp_me->email = strdup(argv[i]); else if (!strcmp(colname[i], "env")) userexist_tmp_me->env = strdup(argv[i]); +# ifdef USE_PBKDF2 + else if (!strcmp(colname[i], "salt")) + userexist_tmp_me->salt = strdup(argv[i]); +# endif else if (!strcmp(colname[i], "password")) userexist_tmp_me->password = strdup(argv[i]); else if (!strcmp(colname[i], "flags")) @@ -2280,6 +2496,9 @@ userexist (char *cname, int isnew) free(userexist_tmp_me->email); free(userexist_tmp_me->env); free(userexist_tmp_me->password); +# ifdef USE_PBKDF2 + free(userexist_tmp_me->salt); +# endif free(userexist_tmp_me); userexist_tmp_me = NULL; } @@ -2449,13 +2668,24 @@ writefile (int requirenew) int ret, retry = 10; char *qbuf; + + # ifdef USE_PBKDF2 + if (requirenew) { + qbuf = sqlite3_mprintf("insert into dglusers (username, email, env, salt, password, flags) values ('%q', '%q', '%q', '%q', '%q', %li)", me->username, me->email, me->env, me->salt, me->password, me->flags); + } else { + qbuf = sqlite3_mprintf("update dglusers set username='%q', email='%q', env='%q', salt='%q', password='%q', flags=%li where id=%i", me->username, me->email, me->env, me->salt, me->password, me->flags, me->id); + } + # else if (requirenew) { - qbuf = sqlite3_mprintf("insert into dglusers (username, email, env, password, flags) values ('%q', '%q', '%q', '%q', %li)", me->username, me->email, me->env, me->password, me->flags); + qbuf = sqlite3_mprintf("insert into dglusers (username, email, env, password, flags) values ('%q', '%q', '%q', '%q', %li)", me->username, me->email, me->env, me->password, me->flags); } else { - qbuf = sqlite3_mprintf("update dglusers set username='%q', email='%q', env='%q', password='%q', flags=%li where id=%i", me->username, me->email, me->env, me->password, me->flags, me->id); + qbuf = sqlite3_mprintf("update dglusers set username='%q', email='%q', env='%q', password='%q', flags=%li where id=%i", me->username, me->email, me->env, me->password, me->flags, me->id); } + + # endif + ret = sqlite3_open(globalconfig.passwd, &db); if (ret) { sqlite3_close(db); @@ -2970,3 +3200,169 @@ main (int argc, char** argv) return 1; } + + +//converts ascii representation of hash to bytes +//in unsigned char array +int ascii_to_hex(char *input, unsigned char* output, int keyLen) +{ + + char chunk[2]; + int i; + + if(strlen(input) < keyLen*2) + { + return 0; + } + + for (i =0; i < keyLen; i++) + { + + chunk[0] = input[2*i]; + chunk[1] = input[2*i+1]; + sprintf(output + i, "%c", (unsigned char)strtol(chunk, NULL, 16)); + + } + + return 1; + +} + +//converts byte representation of hash to ascii +//representation in char array +int hex_to_ascii(unsigned char* input, char* output, int keyLen) +{ + + int i; + + for(i = 0; i < keyLen; i++) + { + sprintf(output + (i * 2), "%02x", input[i]); + + } + output[2*keyLen] = '\0'; + + return 1; + +} + + +int memset_s(void *v, int c, size_t n) { + if (v == NULL) return EINVAL; + + volatile unsigned char *p = v; + while (n--) { + *p++ = c; + } + + return 0; +} + +/* if me, free */ +/* get username, check ifalnum */ + +/* check if username has salt */ + +/* if has salt, ignore, but go through following steps */ + +/* get oldpw, check w/ crypt */ + +/* if error w/ passwords or salt, exit */ + + +#ifdef USE_PBKDF2 +void updatepw() +{ + + char user_buf[DGL_PLAYERNAMELEN+1]; + char* pw_buf; + char *crypted; + + int error = 2; + + loggedin = 0; + + while (error) + { + clear (); + + drawbanner (&banner); + + mvaddstr (5, 1, + "Please enter your username. (blank entry aborts)"); + mvaddstr (7, 1, "=> "); + + if (error == 1) + { + mvaddstr (9, 1, "There was a problem with your last entry."); + move (7, 4); + } + + refresh (); + + if (mygetnstr (user_buf, DGL_PLAYERNAMELEN, 1) != OK) + return; + + if (*user_buf == '\0') + return; + + error = 1; + + { + struct dg_user *tmpme; + if ((tmpme = userexist(user_buf, 0))) { + me = cpy_me(tmpme); + error = 0; + } + } + } + + clear (); + + drawbanner (&banner); + + pw_buf = (char*)malloc((DGL_PASSWDLEN+2)* sizeof(char)); + + mvaddstr (5, 1, "Please enter your password."); + mvaddstr (7, 1, "=> "); + + refresh (); + + if (mygetnstr (pw_buf, DGL_PASSWDLEN, 0) != OK){ + memset_s(pw_buf, 0, strlen(pw_buf)); + free(pw_buf); + return; + } + + crypted = crypt (pw_buf, pw_buf); + memset_s(pw_buf, 0, strlen(pw_buf)); + free(pw_buf); + + if (strlen(me->salt) || crypted==NULL || strncmp(crypted, me->password, DGL_PASSWDLEN)){ + + clear (); + drawbanner (&banner); + mvaddstr (5, 1, "There was a problem updating your password."); + mvaddstr (6, 1, "Either old password is incorrect or password already updated."); + refresh (); + dgl_getch(); + return; + } + + + if (me->flags & DGLACCT_LOGIN_LOCK) { + clear (); + mvprintw(5, 1, "Sorry, that account has been banned.--More--"); + refresh (); + dgl_getch(); + return; + } + + + changepw(1); + loggedin = 1; + setproctitle("%s", me->username); + dgl_exec_cmdqueue(globalconfig.cmdqueue[DGLTIME_LOGIN], 0, me); + return; +} +#endif diff --git a/dgamelaunch.h b/dgamelaunch.h index e298adf..435d3c3 100644 --- a/dgamelaunch.h +++ b/dgamelaunch.h @@ -20,9 +20,16 @@ #define dglsign(x) (x < 0 ? -1 : (x > 0 ? 1 : 0)) #define DGL_PLAYERNAMELEN 30 /* max. length of player name */ -#define DGL_PASSWDLEN 20 /* max. length of passwords */ +#define DGL_PASSWDLEN 32 /* max. length of passwords */ #define DGL_MAILMSGLEN 80 /* max. length of mail message */ +/* crypto stuff */ +#ifdef USE_PBKDF2 +# define DGL_SALTLEN 32 +# define DGL_ITERATION 200000 +# define DGL_KEYLEN 32 +#endif + #define DGL_MAXWATCHCOLS 10 #define DGL_BANNER_LINELEN 256 /* max. length of banner lines*/ @@ -74,7 +81,8 @@ typedef enum DGLCMD_PLAYGAME, /* play_game "foo" */ DGLCMD_PLAY_IF_EXIST, /* play_if_exist "game" "file" */ DGLCMD_SUBMENU, /* submenu "foo" */ - DGLCMD_RETURN /* return */ + DGLCMD_RETURN, /* return */ + DGLCMD_UPDATEPW /* updates password for new pbkdf2 database */ } dglcmd_actions; typedef enum @@ -131,6 +139,9 @@ struct dg_user char *email; char *env; char *password; +#ifdef USE_PBKDF2 + char *salt; +#endif int flags; /* dgl_acct_flag bitmask */ }; @@ -364,3 +375,9 @@ extern size_t strlcat (char *dst, const char *src, size_t siz); extern int mygetnstr(char *buf, int maxlen, int doecho); #endif + +/* crypto stuff */ +extern int ascii_to_byte(char *input, unsigned char* output, int keyLen); +extern int byte_to_ascii(unsigned char* input, char* output, int keyLen); +extern int memset_s(void *v, int c, size_t n); +extern void updatepw(); diff --git a/dgl-common.c b/dgl-common.c index 80560b5..0b5683d 100644 --- a/dgl-common.c +++ b/dgl-common.c @@ -329,6 +329,9 @@ dgl_exec_cmdqueue(struct dg_cmdpart *queue, int game, struct dg_user *me) case DGLCMD_REGISTER: if (!loggedin && globalconfig.allow_registration) newuser(); break; + case DGLCMD_UPDATEPW: + if (!loggedin) updatepw(); + break; case DGLCMD_QUIT: debug_write("command: quit"); graceful_exit(0); diff --git a/dgl-create-chroot b/dgl-create-chroot index a24a19d..86155de 100755 --- a/dgl-create-chroot +++ b/dgl-create-chroot @@ -104,7 +104,7 @@ if [ -n "$SQLITE_DBFILE" ]; then echo "Creating SQLite database at $SQLITE_DBFILE" SQLITE_DBFILE="`echo ${SQLITE_DBFILE%/}`" SQLITE_DBFILE="`echo ${SQLITE_DBFILE#/}`" - sqlite3 "$CHROOT/$SQLITE_DBFILE" "create table dglusers (id integer primary key, username text, email text, env text, password text, flags integer);" + sqlite3 "$CHROOT/$SQLITE_DBFILE" "create table dglusers (id integer primary key, username text, email text, env text, salt text, password text, flags integer);" chown "$USRGRP" "$CHROOT/$SQLITE_DBFILE" fi fi diff --git a/examples/dgamelaunch.conf b/examples/dgamelaunch.conf index 2a79175..0679289 100644 --- a/examples/dgamelaunch.conf +++ b/examples/dgamelaunch.conf @@ -141,6 +141,8 @@ default_term = "xterm" # ask_login = do the login prompting, if not logged in # ask_register = do register new user prompting, if not logged in and # registration of new nicks is allowed. +# update_passwd = prompts user for old password to create new +# password using pbkdf2 hashing # play_game "foo" = start game which has the short name "foo" # (user must be logged in) # play_if_exist "foo" "file" = start game "foo", if file "file" exists.