<?php $INC = isset($INC) ? $INC : dirname(__FILE__); require_once($INC . "/util.inc.php"); require_once($INC . "/log.inc.php"); require_once($INC . "/sql.inc.php"); require_once($INC . "/events.inc.php"); $rg_totp_error = ''; function rg_totp_set_error($str) { global $rg_totp_error; $rg_totp_error = $str; rg_log('totp_set_error: ' . $str); } function rg_totp_error() { global $rg_totp_error; return $rg_totp_error; } /* * Event functions */ $rg_totp_functions = array( //2006 => "rg_totp_link_by_name" ); rg_event_register_functions($rg_totp_functions); $rg_totp_base32_tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; /* * Generates a secret (returned as google base32 string) */ function rg_totp_base32_generate($digits) { global $rg_totp_base32_tab; $ret = ''; for ($i = 0; $i < $digits; $i++) $ret .= $rg_totp_base32_tab[rand(0, 31)]; return $ret; } /* * Transforms a base32 google string into a binary array */ function rg_totp_base32_decode($s) { global $rg_totp_base32_tab; $s = strtoupper($s); $s_len = strlen($s); $ret = ''; $buf = 0; $buf_bits = 0; $i = 0; while ($i < $s_len) { while (($i < $s_len) && ($buf_bits < 8)) { $n = strpos($rg_totp_base32_tab, $s[$i++]); $buf = ($buf << 5) | $n; $buf_bits += 5; } // less than 8 bits left, pad right with 0 if ($buf_bits < 8) { $buf <<= (8 - $buf_bits); $buf_bits = 8; } // Now, we have more than 8 bits in buffer, extract // next out char. $buf_bits -= 8; $c = chr($buf >> $buf_bits); $ret .= $c; $buf &= ((1 << $buf_bits) - 1); } return $ret; } function rg_totp_compute($key, $tc, $digits) { $key_bin = rg_totp_base32_decode($key); $tc_bin = hex2bin(sprintf("%016x", $tc)); $h = hash_hmac('sha1', $tc_bin, $key_bin, TRUE /*bin output*/); $o = ord($h[strlen($h) - 1]) & 0x0F; $i = (ord($h[$o]) << 24) | (ord($h[$o + 1]) << 16) | (ord($h[$o + 2]) << 8) | ord($h[$o + 3]); $i &= 0x7FFFFFFF; return sprintf("%0" . $digits . "u", $i % pow(10, $digits)); } /* * Verifies a tokens based on a specified 'tc' * Returns 'tc' if ok, FALSE on error */ function rg_totp_verify_tc($key, $tc, $token) { $t = rg_totp_compute($key, $tc, 6); //rg_log('DEBUG: compute[tc=' . $tc . ']=' . $t); if (strcmp($token, $t) == 0) return $tc; return FALSE; } /* * Verifies if a login token is valid * We try 2 tcs before current ts and after current ts * Returns FALSE if token is not valid or 'tc' of the token if is valid. */ function rg_totp_verify($key, $ts, $token) { rg_prof_start('totp_verify'); rg_log_enter('totp_verify ts=' . $ts . ', token=' . $token); $ret = FALSE; while (1) { if (empty($token)) break; $token = sprintf("%06u", $token); $tc = intval($ts / 30); $list = array($tc, $tc - 1, $tc - 2, $tc + 1, $tc + 2); foreach ($list as $tc) { $ret = rg_totp_verify_tc($key, $tc, $token); //rg_log('DEBUG: using tc ' . $tc . ', ret=' . $ret); if ($ret !== FALSE) break; } break; } rg_log_exit(); rg_prof_end('totp_verify'); return $ret; } /* * Returns a PNG encoded QR code */ function rg_totp_png($secret) { global $rg_ssh_host; $extra = gmdate('Y-m-d H:i'); $issuer = $rg_ssh_host; $cmd = "qrencode -o - --level=H --type=PNG 'otpauth://totp/$extra?secret=$secret&issuer=$issuer'"; $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) return FALSE; return $a['data']; } /* * Returns a UTF-8 encoded QR code */ function rg_totp_text($secret) { global $rg_ssh_host; $extra = gmdate('Y-m-d H:i'); $issuer = $rg_ssh_host; $cmd = "qrencode -o - --level=M --margin=2 --type=UTF8 'otpauth://totp/$extra?secret=$secret&issuer=$issuer'"; $a = rg_exec($cmd, '', FALSE, FALSE); if ($a['ok'] != 1) return FALSE; return $a['data']; } /* * Cosmetic fixes for a login token row */ function rg_totp_cosmetic(&$row) { if (isset($row['itime'])) $row['itime_nice'] = gmdate('Y-m-d H:i', $row['itime']); if (!isset($row['used'])) $row['used'] = 0; if ($row['used'] > 0) $row['used_nice'] = gmdate('Y-m-d H:i', $row['used']); else $row['used_nice'] = "n/a"; if (isset($row['conf'])) { if (strcmp($row['conf'], 't') == 0) $row['conf_nice'] = 'confirmed'; else $row['conf_nice'] = 'pending'; } } /* * Cosmetic fixes for a scratch token row */ function rg_totp_sc_cosmetic(&$row) { if (isset($row['itime'])) $row['itime_nice'] = gmdate('Y-m-d H:i', $row['itime']); } /* * Returns if the user is enrolled or not */ function rg_totp_enrolled($db, $uid) { $ret = array(); $ret['ok'] = 0; $ret['enrolled'] = 1; while (1) { $lt = rg_totp_device_list($db, $uid); if ($lt['ok'] != 1) break; // We will not consider unconfirmed entries as enrollment foreach ($lt['list'] as $t) { if (strcmp($t['conf'], 't') == 0) { $ret['ok'] = 1; return $ret; } } $sc = rg_totp_sc_list($db, $uid); if ($sc['ok'] != 1) break; if (!empty($sc['list'])) { $ret['ok'] = 1; return $ret; } $ret['enrolled'] = 0; $ret['ok'] = 1; break; } return $ret; } /* * Sets when a login token was last used */ function rg_totp_set_last_use($db, $uid, $id, $tc, $ts) { rg_prof_start('totp_set_last_use'); rg_log_enter('totp_set_last_use uid=' . $uid . ' id=' . $id . ' tc=' . $tc); $ret = FALSE; while (1) { $params = array('uid' => $uid, 'id' => $id, 'used' => $ts, 'last_used_tc' => $tc, 'conf' => 't'); $sql = 'UPDATE login_tokens SET used = @@used@@' . ', last_used_tc = @@last_used_tc@@' . ', conf = @@conf@@' . ' WHERE uid = @@uid@@' . ' AND id = @@id@@'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot update last used (' . rg_sql_error()); break; } rg_sql_free_result($res); $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'device' . '::' . $id; $a = array('used' => $ts, 'last_used_tc' => $tc, 'conf' => 't'); rg_totp_cosmetic($a); rg_cache_merge($key, $a, RG_SOCKET_NO_WAIT); $ret = TRUE; break; } rg_log_exit(); rg_prof_end('totp_set_last_use'); return $ret; } /* * Returns a list of login tokens from database */ function rg_totp_device_list($db, $uid) { rg_prof_start('totp_device_list'); rg_log_enter('totp_device_list'); $ret = array(); $ret['ok'] = 0; while (1) { $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'device'; $r = rg_cache_get($key); if ($r !== FALSE) { $ret['list'] = $r; $ret['ok'] = 1; break; } $params = array('uid' => $uid); $sql = 'SELECT * FROM login_tokens' . ' WHERE uid = @@uid@@' . ' ORDER BY itime'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot load login tokens'); break; } $ret['list'] = array(); while (($row = rg_sql_fetch_array($res))) { $id = $row['id']; rg_totp_cosmetic($row); $ret['list'][$id] = $row; } rg_sql_free_result($res); rg_cache_set($key, $ret['list'], RG_SOCKET_NO_WAIT); $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end('totp_device_list'); return $ret; } /* * Validates a device token (not a scratch code) * Also, it marks the tokens as 'confirmed' if needed */ function rg_totp_device_verify($db, $uid, $token) { rg_prof_start('totp_device_verify'); rg_log_enter('totp_device_verify token=' . $token); $now = time(); $ret = array(); $ret['ok'] = 0; $ret['enrolled'] = 0; $ret['token_valid'] = 0; $ret['id'] = 0; $ret['reuse'] = 0; while (1) { $lt = rg_totp_device_list($db, $uid); if ($lt['ok'] != 1) break; $ret['ok'] = 1; $err_set = FALSE; foreach ($lt['list'] as $t) { if (strcmp($t['conf'], 't') == 0) if ($ret['enrolled'] == 0) $ret['enrolled'] = 1; $tc = rg_totp_verify($t['secret'], $now, $token); if ($tc === FALSE) continue; if ($tc <= $t['last_used_tc']) { $ret['reuse'] = 1; break; } $ret['token_valid'] = 1; $ret['id'] = $t['id']; // Mark it as used and update 'conf' status $r = rg_totp_set_last_use($db, $uid, $t['id'], $tc, $now); if ($r !== TRUE) { $err_set = TRUE; $ret['ok'] = 0; break; } // We just confirmed an unconf entry, so we are enrolled if ($ret['enrolled'] == 0) $ret['enrolled'] = 1; break; } if ($err_set) { // Do nothing } else if ($ret['reuse'] == 1) { rg_totp_set_error('cannot reuse the login token'); } else if ($ret['enrolled'] == 0) { rg_totp_set_error('you are not enrolled'); } else if ($ret['token_valid'] != 1) { rg_totp_set_error('invalid token; sync the time'); } break; } rg_log_exit(); rg_prof_end('totp_device_verify'); return $ret; } /* * Add a new secret login token to database */ function rg_totp_enroll($db, $uid, $name, $secret, $ip, $conf) { rg_prof_start('totp_enroll'); rg_log_enter('totp_enroll name=' . $name); $ret = FALSE; while (1) { $params = array('uid' => $uid, 'name' => $name, 'secret' => $secret, 'itime' => time(), 'ip' => $ip, 'conf' => $conf, 'last_used_tc' => 0); $sql = 'INSERT INTO login_tokens (uid, itime, name, secret, ip' . ', conf, last_used_tc)' . ' VALUES (@@uid@@, @@itime@@, @@name@@' . ', @@secret@@, @@ip@@, @@conf@@, @@last_used_tc@@)' . ' RETURNING id'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot insert login token; try again later'); break; } $row = rg_sql_fetch_array($res); rg_sql_free_result($res); $params['id'] = $row['id']; rg_totp_cosmetic($params); $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'device' . '::' . $params['id']; rg_cache_set($key, $params, RG_SOCKET_NO_WAIT); $ret = TRUE; break; } rg_log_exit(); rg_prof_end('totp_enroll'); return $ret; } /* * Add an ip to login_tokens_ip table */ function rg_totp_add_ip($db, $uid, $token_id, $ip, $expire_ts) { rg_prof_start('totp_add_ip'); rg_log_enter('totp_add_ip ip=' . $ip . ' expire_ts=' . $expire_ts); $ret = FALSE; while (1) { $params = array('uid' => $uid, 'token_id' => $token_id, 'ip' => $ip, 'itime' => time(), 'expire' => $expire_ts); $sql = 'INSERT INTO login_tokens_ip' . ' (uid, ip, itime, expire, token_id)' . ' VALUES (@@uid@@, @@ip@@, @@itime@@, @@expire@@' . ', @@token_id@@)'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot insert login token ip; try again later'); break; } rg_sql_free_result($res); unset($params['uid']); $eip = str_replace(':', '_', $ip); $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'ip' . '::' . $eip; rg_cache_set($key, $params, RG_SOCKET_NO_WAIT); $ret = TRUE; break; } rg_log_exit(); rg_prof_end('totp_add_ip'); return $ret; } /* * Deletes an ip from login_tokens_ip table */ function rg_totp_del_ip($db, $uid, $ip) { rg_prof_start('totp_del_ip'); rg_log_enter('totp_del_ip ip=' . $ip); $ret = array(); $ret['ok'] = 0; $ret['found'] = 0; while (1) { $params = array('uid' => $uid, 'ip' => $ip); if (strcasecmp($ip, 'all') == 0) { $sql = 'DELETE FROM login_tokens_ip' . ' WHERE uid = @@uid@@'; } else { $sql = 'DELETE FROM login_tokens_ip' . ' WHERE uid = @@uid@@' . ' AND ip = @@ip@@'; } $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot delete ip; try again later'); break; } $aff = rg_sql_affected_rows($res); rg_sql_free_result($res); $ret['ok'] = 1; if ($aff == 0) { rg_totp_set_error('ip not found'); break; } $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'ip'; if (strcasecmp($ip, 'all') != 0) { $eip = str_replace(':', '_', $ip); $key .= '::' . $eip; } rg_cache_unset($key, RG_SOCKET_NO_WAIT); $ret['found'] = 1; break; } rg_log_exit(); rg_prof_end('totp_del_ip'); return $ret; } /* * Returns a list of login tokens IPs from database */ function rg_totp_list_ip($db, $uid) { rg_prof_start('totp_list_ip'); rg_log_enter('totp_list_ip'); while (1) { $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'ip'; $list = rg_cache_get($key); if ($list !== FALSE) { $ret['list'] = $list; $ret['ok'] = 1; break; } $ret = array(); $ret['ok'] = 0; $params = array('uid' => $uid, 'now' => time()); $sql = 'SELECT * FROM login_tokens_ip' . ' WHERE uid = @@uid@@' . ' AND expire >= @@now@@' . ' ORDER BY itime'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot load login tokens ip'); break; } $ret['list'] = array(); while (($row = rg_sql_fetch_array($res))) { unset($row['uid']); $eip = str_replace(':', '_', $row['ip']); $ret['list'][$eip] = $row; } rg_sql_free_result($res); rg_cache_set($key, $ret['list'], RG_SOCKET_NO_WAIT); $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end('totp_list_ip'); return $ret; } /* * Verifies that an IP is in the 'validated' list * Returns ip_list to be used for an optional dump of the list. */ function rg_totp_verify_ip($db, $uid, $ip) { rg_prof_start('totp_verify_ip'); rg_log_enter('totp_verify_ip ip=' . $ip); $ret = array(); $ret['ok'] = 0; $ret['enrolled'] = 1; $ret['ip_list'] = array(); while (1) { $r = rg_totp_enrolled($db, $uid); if ($r['ok'] != 1) break; if ($r['enrolled'] == 0) { $ret['enrolled'] = 0; $ret['ok'] = 1; break; } $r = rg_totp_list_ip($db, $uid); if ($r['ok'] != 1) break; foreach ($r['list'] as $eip => $t) { if (strcasecmp($t['ip'], $ip) == 0) { $ret['ip_list'] = $r['list']; $ret['ok'] = 1; break; } } if (empty($ret['ip_list'])) rg_totp_set_error('you have no IP validated;' . ' run \'ssh ... totp\' for help'); break; } rg_log_exit(); rg_prof_end('totp_verify_ip'); return $ret; } /* * Remove a list of login tokens */ function rg_totp_remove($db, $uid, $list) { rg_prof_start('totp_remove'); rg_log_enter('totp_remove uid=' . $uid . ' list=' . rg_array2string($list)); $ret = FALSE; while (1) { if (empty($list)) { rg_totp_set_error('you did not select anything'); break; } $my_list = array(); foreach ($list as $id => $junk) $my_list[] = sprintf("%u", $id); $params = array('uid' => $uid); $sql_list = implode(', ', $my_list); $sql = 'DELETE FROM login_tokens_ip' . ' WHERE uid = @@uid@@' . ' AND token_id IN (' . $sql_list . ')'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot remove login tokens ips'); break; } rg_sql_free_result($res); $sql = 'DELETE FROM login_tokens' . ' WHERE uid = @@uid@@' . ' AND id IN (' . $sql_list . ')'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot remove login token'); break; } rg_sql_free_result($res); foreach ($my_list as $junk => $_id) { $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'device' . '::' . $_id; rg_cache_unset($key, RG_SOCKET_NO_WAIT); } $ret = TRUE; break; } rg_log_exit(); rg_prof_end('totp_remove'); return $ret; } /* * Validates a scratch code * TODO: it deletes the used code. */ function rg_totp_sc_verify($db, $uid, $token) { rg_prof_start('totp_sc_verify'); rg_log_enter('totp_sc_verify token=' . $token); $now = time(); $token = sprintf("%08u", $token); $ret = array(); $ret['ok'] = 0; $ret['enrolled'] = 0; $ret['token_valid'] = 0; $ret['id'] = 0; // we do not have an id for scratch codes; but, we may // be forced to use one to delete the IPs associated. while (1) { $sc = rg_totp_sc_list($db, $uid); if ($sc['ok'] != 1) break; $ret['ok'] = 1; $done = FALSE; foreach ($sc['list'] as $itime => $per_itime) { if ($ret['enrolled'] == 0) $ret['enrolled'] = 1; foreach ($per_itime as $sc => $junk) { rg_log('DEBUG: comparing with ' . $sc); if (strcmp($sc, $token) == 0) { $r = rg_totp_sc_remove($db, $uid, $itime, $token); if ($r !== TRUE) $ret['ok'] = 0; else $ret['token_valid'] = 1; $done = TRUE; break; } } if ($done) break; } if ($ret['enrolled'] == 0) rg_totp_set_error('you are not enrolled'); else if ($ret['token_valid'] != 1) rg_totp_set_error('invalid token'); break; } rg_log_ml('DEBUG: sc_verify returns: ' . print_r($ret, TRUE)); rg_log_exit(); rg_prof_end('totp_sc_verify'); return $ret; } /* * List the scratch codes */ function rg_totp_sc_list($db, $uid) { rg_prof_start('totp_sc_list'); rg_log_enter('totp_sc_list'); $ret = array(); $ret['ok'] = 0; while (1) { $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'sc'; $r = rg_cache_get($key); if ($r !== FALSE) { $ret['list'] = $r; $ret['ok'] = 1; break; } $params = array('uid' => $uid); $sql = 'SELECT * FROM scratch_codes' . ' WHERE uid = @@uid@@' . ' ORDER BY itime DESC'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot load scratch codes'); break; } $ret['list'] = array(); while (($row = rg_sql_fetch_array($res))) { $itime = $row['itime']; $sc = $row['sc']; if (!isset($ret['list'][$itime])) $ret['list'][$itime] = array(); $ret['list'][$itime][$sc] = 1; } rg_sql_free_result($res); rg_cache_set($key, $ret['list'], RG_SOCKET_NO_WAIT); $ret['ok'] = 1; break; } //rg_log_ml('DEBUG: sc_list ret[list]: ' . print_r($ret['list'], TRUE)); rg_log_exit(); rg_prof_end('totp_sc_list'); return $ret; } /* * Generates a list of scratch codes for a user */ function rg_totp_sc_generate($db, $uid, $count) { rg_prof_start('totp_sc_generate'); rg_log_enter('totp_sc_generate count=' . $count); $ret = array(); $ret['ok'] = 0; $ret['list'] = array(); while (1) { $bin = rg_random_bytes($count * 4); if ($bin === FALSE) break; for ($i = 0; $i < $count; $i++) { $t = substr($bin, $i * 4, 4); $t2 = unpack('L', $t); $t2 = $t2[1] % 100000000; $sc = sprintf('%08u', $t2); $ret['list'][$sc] = 1; } $count -= count($ret['list']); if ($count != 0) continue; $now = time(); $params = array('uid' => $uid, 'itime' => $now); $sql_add = ''; $add = ''; $i = 0; foreach ($ret['list'] as $sc => $junk) { $params['token_' . $i] = $sc; $sql_add .= $add . '(@@uid@@, @@itime@@, @@token_' . $i . '@@)'; $add = ','; $i++; } $sql = 'INSERT INTO scratch_codes (uid, itime, sc)' . ' VALUES ' . $sql_add; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot insert scratch codes; try again later'); break; } rg_sql_free_result($res); rg_totp_cosmetic($params); $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'sc' . '::' . $now; rg_cache_set($key, $ret['list'], RG_SOCKET_NO_WAIT); $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end('totp_sc_generate'); return $ret; } /* * Removes one scratch code */ function rg_totp_sc_remove($db, $uid, $itime, $token) { rg_prof_start('totp_sc_remove'); rg_log_enter('totp_sc_remove uid=' . $uid . ' token=' . $token); $ret = FALSE; while (1) { $params = array('uid' => $uid , 'itime' => $itime, 'token' => $token); $sql = 'DELETE FROM scratch_codes' . ' WHERE uid = @@uid@@' . ' AND itime = @@itime@@' . ' AND sc = @@token@@'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot remove scratch code'); break; } rg_sql_free_result($res); $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'sc' . '::' . $itime . '::' . $token; rg_cache_unset($key, RG_SOCKET_NO_WAIT); $ret = TRUE; break; } rg_log_exit(); rg_prof_end('totp_sc_remove'); return $ret; } /* * Remove a list of scratch codes */ function rg_totp_sc_remove_list($db, $uid, $list) { rg_prof_start('totp_sc_remove_list'); rg_log_enter('totp_sc_remove_list uid=' . $uid . ' list=' . rg_array2string($list)); $ret = FALSE; while (1) { if (empty($list)) { rg_totp_set_error('you did not select anything'); break; } $my_list = array(); foreach ($list as $id => $junk) $my_list[] = sprintf("%u", $id); $params = array('uid' => $uid); $sql_list = implode(', ', $my_list); $sql = 'DELETE FROM scratch_codes' . ' WHERE uid = @@uid@@' . ' AND itime IN (' . $sql_list . ')'; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_totp_set_error('cannot remove scratch codes'); break; } rg_sql_free_result($res); foreach ($my_list as $junk => $_itime) { $key = 'user' . '::' . $uid . '::' . 'login_tokens' . '::' . 'sc' . '::' . $_itime; rg_cache_unset($key, RG_SOCKET_NO_WAIT); } $ret = TRUE; break; } rg_log_exit(); rg_prof_end('totp_sc_remove_list'); return $ret; } /* * Unenroll function - clean everything */ function rg_totp_unenroll($db, $uid) { rg_prof_start('totp_unenroll'); rg_log_enter('totp_unenroll'); $ret = FALSE; $rollback = FALSE; while (1) { $key = 'user' . '::' . $uid . '::' . 'login_tokens'; $r = rg_cache_unset($key, 0); if ($r === FALSE) { rg_totp_set_error('cannot clear cache'); break; } $r = rg_sql_begin($db); if ($r === FALSE) { rg_totp_set_error('cannot start transaction'); break; } $rollback = TRUE; $r = rg_totp_del_ip($db, $uid, "all"); if ($r['ok'] != 1) break; $ok = TRUE; $params = array('uid' => $uid); $list = array('login_tokens', 'login_tokens_ip', 'scratch_codes'); foreach ($list as $t) { $sql = 'DELETE FROM ' . $t . ' WHERE uid = @@uid@@'; $res = rg_sql_query_params($db, $sql, $params); if (!$res) { rg_totp_set_error('cannot delete login tokens information'); $ok = FALSE; break; } } if (!$ok) break; rg_sql_commit($db); $ret = TRUE; $rollback = FALSE; break; } if ($rollback) rg_sql_rollback($db); rg_log_exit(); rg_prof_end('totp_unenroll'); return $ret; } /* * Verifies either a login token generated by a device or scratch codes */ function rg_totp_verify_any($db, $uid, $token) { rg_prof_start('totp_verify_any'); rg_log_enter('totp_verify_any token=' . $token); $ret = array(); $ret['ok'] = 1; $ret['id'] = 0; $ret['token_valid'] = 0; $ret['enrolled'] = 0; while (1) { $r = rg_totp_device_verify($db, $uid, $token); if ($r['ok'] != 1) { $ret['ok'] = 0; break; } if ($r['enrolled'] == 1) $ret['enrolled'] = 1; if ($r['reuse'] == 1) break; if ($r['token_valid'] == 1) { $ret['token_valid'] = 1; $ret['id'] = $r['id']; break; } $r = rg_totp_sc_verify($db, $uid, $token); if ($r['ok'] != 1) { $ret['ok'] = 0; break; } if ($r['enrolled'] == 1) $ret['enrolled'] = 1; if ($r['token_valid'] == 1) { $ret['token_valid'] = 1; break; } break; } rg_log_ml("DEBUG: verify_any returns: " . print_r($ret, TRUE)); rg_log_exit(); rg_prof_end('totp_verify_any'); return $ret; } /* * High-level function for scratch codes */ function rg_totp_sc_high_level($db, $rg, $paras) { rg_prof_start('totp_sc_high_level'); rg_log_enter('totp_sc_high_level'); $ret = ''; $rg['HTML:gen_errmsg'] = ''; $gen_errmsg = array(); $generate = rg_var_uint('generate'); while ($generate == 1) { if (!rg_valid_referer()) { $gen_errmsg[] = 'invalid referer; try again'; break; } if (!rg_token_valid($db, $rg, 'sc', FALSE)) { $gen_errmsg[] = 'invalid token; try again.'; break; } $r = rg_totp_sc_generate($db, $rg['login_ui']['uid'], 20); if ($r['ok'] != 1) { $gen_errmsg[] = rg_totp_error(); break; } $rg['scratch_codes'] = implode(' ', array_keys($r['list'])); $ret .= rg_template('user/settings/totp/sc/gen_ok.html', $rg, TRUE /*xss*/); break; } $rg['HTML:gen_errmsg'] = rg_template_errmsg($gen_errmsg); $rg['HTML:del_errmsg'] = ''; $rg['HTML:del_status'] = ''; $del_errmsg = array(); $delete = rg_var_uint('delete'); while ($delete == 1) { if (!rg_valid_referer()) { $del_errmsg[] = 'invalid referer; try again'; break; } if (!rg_token_valid($db, $rg, 'sc', FALSE)) { $del_errmsg[] = 'invalid token; try again.'; break; } $list = rg_var_str("delete_list"); $r = rg_totp_sc_remove_list($db, $rg['login_ui']['uid'], $list); if ($r !== TRUE) { $del_errmsg[] = 'cannot delete: ' . rg_totp_error(); break; } $rg['HTML:del_status'] = rg_template('user/settings/totp/sc/delete_ok.html', $rg, TRUE /*xss*/); break; } $rg['HTML:del_errmsg'] = rg_template_errmsg($del_errmsg); $rg['rg_form_token'] = rg_token_get($db, $rg, 'sc'); $ret .= rg_template('user/settings/totp/sc/gen.html', $rg, TRUE /*xss*/); $r = rg_totp_sc_list($db, $rg['login_ui']['uid']); if ($r['ok'] !== 1) { $rg['totp_errmsg'] = rg_totp_error(); $ret .= rg_template('user/settings/totp/sc/list_err.html', $rg, TRUE /*xss*/); } else { // prepare list $_list = array(); //rg_log_ml('DEBUG: prepare sc list: ' . print_r($r['list'], TRUE)); foreach ($r['list'] as $itime => $per_itime) { foreach ($per_itime as $junk => $sc) { if (!isset($_list[$itime])) { $_list[$itime] = array( 'itime' => $itime, 'sc_count' => 1); rg_totp_sc_cosmetic($_list[$itime]); } else { $_list[$itime]['sc_count']++; } } } $ret .= rg_template_table('user/settings/totp/sc/list', $_list, $rg); } // hints $hints = array(); $hints[]['HTML:hint'] = rg_template("user/settings/totp/sc/hints.html", $rg, TRUE /*xss*/); $ret .= rg_template_table("hints/list", $hints, $rg); rg_log_exit(); rg_prof_end('totp_sc_high_level'); return $ret; } /* * High-level function for listing tokens */ function rg_totp_list_high_level($db, $rg, $paras) { rg_prof_start('totp_list_high_level'); rg_log_enter('totp_list_high_level'); $ret = ''; $del_errmsg = array(); $rg['HTML:del_errmsg'] = ''; $rg['HTML:del_status'] = ''; $delete = rg_var_uint('delete'); while ($delete == 1) { if (!rg_valid_referer()) { $del_errmsg[] = 'invalid referer; try again'; break; } if (!rg_token_valid($db, $rg, 'login_tokens_list', FALSE)) { $del_errmsg[] = 'invalid token; try again.'; break; } $list = rg_var_str("delete_list"); $r = rg_totp_remove($db, $rg['login_ui']['uid'], $list); if ($r !== TRUE) { $del_errmsg[] = 'cannot delete: ' . rg_totp_error(); break; } $rg['HTML:del_status'] = rg_template('user/settings/totp/delete_ok.html', $rg, TRUE /*xss*/); break; } $r = rg_totp_device_list($db, $rg['login_ui']['uid']); if ($r['ok'] !== 1) { $rg['totp_errmsg'] = rg_totp_error(); $ret .= rg_template('user/settings/totp/list_err.html', $rg, TRUE /*xss*/); } else { $rg['rg_form_token'] = rg_token_get($db, $rg, 'login_tokens_list'); $rg['HTML:del_errmsg'] = rg_template_errmsg($del_errmsg); $ret .= rg_template_table('user/settings/totp/list', $r['list'], $rg); } rg_log_exit(); rg_prof_end('totp_list_high_level'); return $ret; } /* * Enroll function for TOTP login token */ function rg_totp_enroll_high_level($db, $rg, $paras) { rg_prof_start('totp_enroll_high_level'); rg_log_enter('totp_enroll_high_level'); $now = time(); $ret = ''; $errmsg = array(); $enroll = rg_var_uint('enroll'); while ($enroll == 1) { $name = rg_var_str('totp::name'); $ver = rg_var_str('totp::ver'); $secret = rg_var_str('totp::secret'); if (strlen($name) == 0) { $errmsg[] = "invalid name"; break; } if (strlen($ver) != 6) { $errmsg[] = "invalid number; you must enter a 6 digit number"; break; } if (!rg_valid_referer()) { $errmsg[] = "invalid referer; try again"; break; } if (!rg_token_valid($db, $rg, 'user_totp_enroll', FALSE)) { $errmsg[] = "invalid token; try again"; break; } $r = rg_totp_verify($secret, $now, $ver); if ($r === FALSE) { $errmsg[] = rg_template('user/settings/totp/ver_error.html', $rg, TRUE /*xss*/); break; } $ip = $_SERVER['REMOTE_ADDR']; $r = rg_totp_enroll($db, $rg['login_ui']['uid'], $name, $secret, $ip, 't'); if ($r !== TRUE) { $errmsg[] = rg_totp_error(); break; } $ret .= rg_template('user/settings/totp/enroll_ok.html', $rg, TRUE /*xss*/); break; } // defaults if ($enroll == 0) { $name = ''; $ver = ''; $secret = rg_totp_base32_generate(16); } $rg['totp'] = array(); $rg['totp']['name'] = $name; $rg['totp']['ver'] = $ver; $rg['totp']['secret'] = $secret; $png = rg_totp_png($rg['totp']['secret']); if ($png === FALSE) { $rg['totp']['img'] = 0; } else { $rg['totp']['img'] = 1; $rg['totp']['png'] = base64_encode($png); } $rg['HTML:errmsg'] = rg_template_errmsg($errmsg); $rg['rg_form_token'] = rg_token_get($db, $rg, 'user_totp_enroll'); $ret .= rg_template('user/settings/totp/enroll.html', $rg, TRUE /*xss*/); rg_log_exit(); rg_prof_end('totp_enroll_high_level'); return $ret; } /* * Main HL function for TOTP login token */ function rg_totp_high_level($db, &$rg, $paras) { rg_prof_start('totp_high_level'); rg_log_enter('totp_high_level'); $now = time(); $ret = ''; $op = empty($paras) ? 'info' : array_shift($paras); $rg['menu']['totp'][$op] = 1; $rg['HTML:menu_level2'] = rg_template('user/settings/totp/menu.html', $rg, TRUE /*xss*/); switch ($op) { case 'info': // we show only the hints // hints $hints = array(); $hints[]['HTML:hint'] = rg_template("user/settings/totp/hints.html", $rg, TRUE /*xss*/); $ret .= rg_template_table("hints/list", $hints, $rg); break; case 'enroll': $ret .= rg_totp_enroll_high_level($db, $rg, $paras); break; case 'sc': $ret .= rg_totp_sc_high_level($db, $rg, $paras); break; default: $ret .= rg_totp_list_high_level($db, $rg, $paras); break; } rg_log_exit(); rg_prof_end('totp_high_level'); return $ret; } ?>