<?php
require_once(__DIR__ . '/util2.inc.php');
require_once(__DIR__ . '/log.inc.php');
require_once(__DIR__ . '/sql.inc.php');
require_once(__DIR__ . '/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;
$extra = urlencode(gmdate('Y-m-d H:i'));
$secret = urlencode($secret);
$issuer = urlencode($rg['hostname']);
$cmd = "qrencode -o - --level=H --type=PNG 'otpauth://totp/$extra?secret=$secret&issuer=$issuer'";
$a = rg_exec($cmd, '', FALSE, 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;
$extra = urlencode(gmdate('Y-m-d H:i'));
$secret = urlencode($secret);
$issuer = urlencode($rg['base_url']);
$cmd = "qrencode -o - --level=M --margin=2 --type=UTF8"
. " 'otpauth://totp/$extra?secret=$secret&issuer=$issuer'";
$a = rg_exec($cmd, '', FALSE, FALSE, FALSE);
if ($a['ok'] != 1)
return FALSE;
return $a['data'];
}
/*
* Cosmetic fixes for a login token row
*/
function rg_totp_cosmetic_row(&$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 login token array
*/
function rg_totp_cosmetic_list(&$a)
{
foreach ($a as $junk => &$row)
rg_totp_cosmetic_row($row);
}
/*
* Cosmetic fixes for a scratch token row
*/
function rg_totp_sc_cosmetic_row(&$row)
{
if (isset($row['itime']))
$row['itime_nice'] = gmdate('Y-m-d H:i', $row['itime']);
}
/*
* Cosmetic fixes for a login token array
*/
function rg_totp_sc_cosmetic_list(&$a)
{
foreach ($a as $junk => &$row)
rg_totp_sc_cosmetic_row($row);
}
/*
* 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_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'];
$ret['list'][$id] = $row;
}
rg_sql_free_result($res);
rg_cache_set($key, $ret['list'], RG_SOCKET_NO_WAIT);
$ret['ok'] = 1;
break;
}
if ($ret['ok'] == 1)
rg_totp_cosmetic_list($ret['list']);
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'];
$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;
}
/*
* Remove expired entries from a list of validations
*/
function rg_totp_list_ip_clean($list)
{
$ret = array();
$now = time();
foreach ($list as $eip => $t) {
if ($t['expire'] < $now)
continue;
$ret[$eip] = $t;
}
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'] = rg_totp_list_ip_clean($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)
{
global $rg_ssh_host;
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 IPs validated;'
. ' run \'ssh rocketgit@' . $rg_ssh_host
. ' 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_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;
}
if ($ret['ok'] == 1)
rg_totp_sc_cosmetic_list($ret['list']);
//rg_log_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);
$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) {
$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', '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;
$key = 'user' . '::' . $uid . '::' . 'login_tokens';
$r = rg_cache_unset($key, 0);
if ($r === FALSE) {
rg_totp_set_error('cannot clear cache');
break;
}
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_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 = '';
$ui_login = rg_ui_login();
$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, $ui_login['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, $ui_login['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, $ui_login['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_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_row($_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 = '';
$ui_login = rg_ui_login();
$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, $ui_login['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, $ui_login['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 = trim(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;
}
$ui_login = rg_ui_login();
$r = rg_totp_enroll($db, $ui_login['uid'],
$name, $secret, rg_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;
}