<?php require_once($INC . "/sql.inc.php"); require_once($INC . "/state.inc.php"); require_once($INC . "/prof.inc.php"); require_once($INC . "/mail.inc.php"); require_once($INC . "/events.inc.php"); $rg_keys_error = ""; function rg_keys_set_error($str) { global $rg_keys_error; $rg_keys_error = $str; rg_log($str); } function rg_keys_error() { global $rg_keys_error; return $rg_keys_error; } /* * Events functions */ $rg_keys_functions = array( 1000 => "rg_keys_event_new", 1001 => "rg_keys_event_del", 1002 => "rg_keys_event_regen", 1003 => "rg_keys_event_notify_user" ); rg_event_register_functions($rg_keys_functions); /* * Event for adding a new key */ function rg_keys_event_new($db, $event) { $ret = array(); $event['op'] = "new"; // mark keys dirty $ret[] = array_merge($event, array("category" => 1002, "prio" => 10)); // notify user $ret[] = array_merge($event, array("category" => 1003, "prio" => 100)); return $ret; } /* * Event for deleting a key */ function rg_keys_event_del($db, $event) { $ret = array(); $event['type'] = 1; $event['op'] = "del"; // mark keys dirty $ret[] = array_merge($event, array("category" => 1002, "prio" => 10)); // notify user $ret[] = array_merge($event, array("category" => 1003, "prio" => 100)); return $ret; } /* * Regenerate keyring. * We ignore requests that were inserted in queue after we already * regenerated the keys. * We must regenerate now to not let the user wait too much. * TODO: When we will have support in sshd for key lookup, we will not need to regenerate. */ function rg_keys_event_regen($db, $event) { rg_log("keys_event_regen"); $last = rg_cache_get("key::last_regen_time"); if ($last === FALSE) $last = 0; if ($event['itime'] < $last) { rg_log("DEBUG: event itime(" . $event['itime'] . ") < last($last)." . " Skip regeneration of keys."); } else { $r = rg_keys_regen($db); if ($r === FALSE) return FALSE; } return array(); } /* * Notify user that a new key was added to the keyring */ function rg_keys_event_notify_user($db, $event) { rg_prof_start("keys_event_notify_user"); rg_log("keys_event_notify_user: event=" . rg_array2string($event)); // TODO: del: Maybe add also the statistics. // TODO: del: Do not forget that here we have a list // TODO: del: Take care: we already deleted the keys. We cannot inspect // them anymore! Maybe put info in the event. $ret = FALSE; while (1) { $r = rg_mail_template("mail/user/key/" . $event['op'], $event); if ($r === FALSE) break; $ret = array(); break; } rg_prof_end("keys_event_notify_user"); return $ret; } /* * Extracts info about a ssh key */ function rg_keys_info($key) { rg_prof_start("keys_info"); rg_log_enter('rg_keys_info key=' . $key); $ret = array(); $ret['ok'] = 0; while(1) { if (empty($key)) { rg_keys_set_error('you did not uploaded the key'); break; } if (strpos($key, "PRIVATE KEY") !== FALSE) { rg_keys_set_error("private instead of public key"); break; } // We must have at least key type and the key $t = explode(' ', $key, 2); if (!isset($t[1])) { rg_keys_set_error("malformed ssh key (missing fields)"); break; } $ret['type'] = $t[0]; if ((strncmp($ret['type'], 'ssh-', 4) != 0) && (strncmp($ret['type'], 'ecdsa-', 6) != 0)) { rg_log('key: ' . $key); rg_keys_set_error("key does not start with ssh- or ecdsa-"); break; } // We try to detect the key because spaces may mess up things $ret['comment'] = ''; $error = TRUE; $off = 0; while (1) { rg_log("DEBUG: off=$off"); // -1 signals that we used the whole string if ($off == -1) break; $pos = strpos($t[1], ' ', $off); if ($pos === FALSE) { $ret['key'] = $t[1]; $off = -1; } else { $ret['key'] = substr($t[1], 0, $pos); $off = $pos + 1; } rg_log("DEBUG: pos=$pos off=$off key=" . $ret['key']); $d = base64_decode($ret['key']); if ($d === FALSE) { rg_keys_set_error("malformed ssh key (base64 failed)"); continue; } $d_len = strlen($d); rg_log("d=" . bin2hex($d)); rg_log("d_len=$d_len"); if (strlen($d) < 4) { rg_keys_set_error("key is too short (< 4)"); continue; } // First, we have the length of the string 'ssh-*' $_t = unpack('N', $d); $len = $_t[1]; rg_log_ml("len=$len"); if ($d_len < 4 + $len) { rg_keys_set_error("key is too short"); continue; } $type2 = substr($d, 4, $len); rg_log("DEBUG: type2=$type2"); if (strcasecmp($ret['type'], $type2) != 0) { rg_keys_set_error('key type mismatch: ' . $ret['type'] . ' != ' . $type2); break; } if (strcasecmp($ret['type'], 'ssh-rsa') == 0) { $count = 2; } else if (strcasecmp($ret['type'], 'ssh-dss') == 0) { $count = 4; } else if (strncasecmp($ret['type'], 'ecdsa-', 6) == 0) { $count = 2; } else if (strcasecmp($ret['type'], 'ssh-ed25519') == 0) { $count = 1; } else { rg_log('Strange key type: ' . $ret['type']); // Probably this is a new key type, just consider it valid $_t = explode(' ', $key, 3); $ret['key'] = $_t[1]; if (isset($_t[2])) $ret['comment'] = $_t[2]; $error = FALSE; break; } $have_all_chunks = TRUE; $used = 4 + $len; for ($i = 0; $i < $count; $i++) { if ($d_len < $used + 4) { rg_keys_set_error('key is too short (chunk length)'); $have_all_chunks = FALSE; break; } $_t = unpack('N', substr($d, $used, 4)); $xlen = $_t[1]; rg_log_ml("xlen=$xlen"); if ($d_len < $used + 4 + $xlen) { rg_keys_set_error("key is too short (chunk body)"); $have_all_chunks = FALSE; break; } $used += 4 + $xlen; } if ($have_all_chunks === FALSE) continue; $ret['comment'] = substr($t[1], $off); $error = FALSE; break; } if ($error) break; $digest = md5($d); $a = array(); for ($i = 0; $i < 16; $i++) $a[] = substr($digest, $i * 2, 2); $ret['fingerprint_md5'] = implode(":", $a); $_x = base64_encode(hash('sha256', $d, TRUE)); $ret['fingerprint_sha256'] = rtrim($_x, "="); $ret['ok'] = 1; break; } rg_log_exit(); rg_prof_end("keys_info"); return $ret; } /* * Remove keys from database for user 'ui' */ function rg_keys_remove($db, $ui, $list) { rg_prof_start("keys_remove"); rg_log_enter("keys_remove: list=" . rg_array2string($list)); $ret = FALSE; while (1) { $my_list = array(); foreach ($list as $key_id => $junk) $my_list[] = sprintf("%u", $key_id); $params = array("uid" => $ui['uid']); $sql_list = implode(", ", $my_list); $sql = "DELETE FROM keys" . " WHERE uid = @@uid@@" . " AND key_id IN (" . $sql_list . ")"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_keys_set_error("cannot delete keys" . " (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $event = array("category" => 1001, "prio" => 50, 'ui' => array('email' => $ui['confirmed'] > 0 ? $ui['email'] : ""), "keys" => implode(",", $my_list)); $r = rg_event_add($db, $event); if ($r !== TRUE) { rg_keys_set_error("cannot add event" . " (" . rg_event_error() . ")"); break; } rg_event_signal_daemon('', 0); $ret = TRUE; break; } rg_log_exit(); rg_prof_end("keys_remove"); return $ret; } /* * Count the number of keys per user */ function rg_keys_count($db, $uid) { rg_prof_start("keys_count"); $ret = FALSE; while (1) { $params = array("uid" => $uid); $sql = "SELECT COUNT(*) AS count FROM keys" . " WHERE uid = @@uid@@"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_keys_set_error("cannot query (" . rg_sql_error() . ")"); break; } $row = rg_sql_fetch_array($res); rg_sql_free_result($res); $ret = $row['count']; break; } rg_prof_end("keys_count"); return $ret; } /* * Add a key * Returns the key_id of the key. */ function rg_keys_add($db, $ui, $key) { global $rg_max_ssh_keys; rg_prof_start("keys_add"); rg_log_enter("keys_add: key=$key"); $ret = FALSE; $do_rollback = 0; while (1) { $itime = time(); $ki = rg_keys_info($key); if ($ki['ok'] != 1) break; // check if we are over the maximum // the config after update may not have this defined. if ($rg_max_ssh_keys == 0) $rg_max_ssh_keys = 10; $no_of_keys = rg_keys_count($db, $ui['uid']); if ($no_of_keys === FALSE) break; if ($no_of_keys >= $rg_max_ssh_keys) { rg_keys_set_error("too many keys" . " (" . $no_of_keys . "); please delete some"); break; } $r = rg_sql_begin($db); if ($r !== TRUE) { rg_keys_set_error("cannot start transaction" . " (" . rg_sql_error() . ")"); break; } $do_rollback = 1; $params = array("itime" => $itime, "uid" => $ui['uid'], "key" => $key); $sql = "INSERT INTO keys (itime, uid, key)" . " VALUES (@@itime@@, @@uid@@, @@key@@)" . " RETURNING key_id"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_keys_set_error("cannot insert key" . " (" . rg_sql_error() . ")"); break; } $row = rg_sql_fetch_array($res); $key_id = $row['key_id']; rg_sql_free_result($res); $event = array("category" => 1000, "prio" => 50, 'ui' => array('email' => $ui['confirmed'] > 0 ? $ui['email'] : ""), 'ki' => $ki, 'key_id' => $key_id); $r = rg_event_add($db, $event); if ($r !== TRUE) { rg_keys_set_error("cannot add event" . " (" . rg_event_error() . ")"); break; } $r = rg_sql_commit($db); if ($r !== TRUE) { rg_keys_set_error("cannot commit transaction" . " (" . rg_sql_error() . ")"); break; } $do_rollback = 0; rg_event_signal_daemon('', 0); $ret = $key_id; break; } if ($do_rollback == 1) rg_sql_rollback($db); rg_log_exit(); rg_prof_end("keys_add"); return $ret; } /* * Update first_use, last_use, last_ip and count */ function rg_keys_update_use($db, $key_id, $ip) { rg_prof_start("keys_update_use"); rg_log_enter("keys_update_use: key_id=$key_id, ip=$ip"); $ret = FALSE; while (1) { $now = time(); $params = array("now" => $now, "key_id" => $key_id, "ip" => $ip); $sql = "UPDATE keys SET first_use = @@now@@" . " WHERE first_use = 0" . " AND key_id = @@key_id@@"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_keys_set_error("cannot update key's first use" . " (" . rg_sql_error() . ")"); break; } $sql = "UPDATE keys SET last_use = @@now@@" . ", last_ip = @@ip@@" . ", count = count + 1" . " WHERE key_id = @@key_id@@"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_keys_set_error("cannot update key" . " (" . rg_sql_error() . ")"); break; } rg_sql_free_result($res); $ret = TRUE; break; } rg_log_exit(); rg_prof_end("keys_update_use"); return $ret; } /* * Regenerates authorized_keys files */ function rg_keys_regen($db) { global $php_errormsg; global $rg_keys_file; global $rg_scripts; global $rg_ssh_paras; rg_prof_start("keys_regen"); $now = time(); $ret = FALSE; while (1) { // create .ssh folder if does not exists $dir = dirname($rg_keys_file); if (!file_exists($dir)) { if (!@mkdir($dir, 0700, TRUE)) { rg_keys_set_error("cannot create dir [$dir] ($php_errormsg)"); break; } chown($dir, "rocketgit"); chgrp($dir, "rocketgit"); } $tmp = $rg_keys_file . ".tmp"; $f = @fopen($tmp, "w"); if ($f === FALSE) { rg_keys_set_error("cannot open file $tmp ($php_errormsg)"); break; } if (chmod($tmp, 0600) === FALSE) { rg_keys_set_error("cannot chmod tmp file ($php_errmsg)"); fclose($f); break; } chown($tmp, "rocketgit"); chgrp($tmp, "rocketgit"); $sql = "SELECT key_id, uid, key FROM keys ORDER BY count DESC"; $res = rg_sql_query($db, $sql); if ($res === FALSE) { rg_keys_set_error("cannot query (" . rg_sql_error() . ")"); break; } $errors = 0; while (($row = rg_sql_fetch_array($res))) { //rg_log("Writing key [" . $row['key'] . "] for uid " . $row['uid']); $buf = "command=\"" . $rg_scripts . "/scripts/remote.sh" . " " . $row['uid'] . " " . $row['key_id'] . "\"" . "," . $rg_ssh_paras . " " . $row['key'] . "\n"; if (@fwrite($f, $buf) === FALSE) { rg_keys_set_error("cannot write; disk space problems? ($php_errormsg)"); $errors = 1; break; } } rg_sql_free_result($res); fclose($f); if ($errors == 1) { unlink($tmp); break; } if (@rename($tmp, $rg_keys_file) === FALSE) { rg_keys_set_error("cannot rename $tmp to $rg_keys_file ($php_errormsg)"); unlink($tmp); break; } rg_cache_set("key::last_regen_time", $now, RG_SOCKET_NO_WAIT); $ret = TRUE; break; } rg_prof_end("keys_regen"); return $ret; } /* * List keys */ function rg_keys_list($db, $ui) { rg_prof_start("keys_list"); rg_log_enter("keys_list: uid=" . $ui['uid']); $ret = FALSE; while (1) { $params = array("uid" => $ui['uid']); $sql = "SELECT * FROM keys WHERE uid = @@uid@@" . " ORDER BY itime DESC"; $res = rg_sql_query_params($db, $sql, $params); if ($res === FALSE) { rg_keys_set_error("Cannot query (" . rg_sql_error() . ")"); break; } $ret = array(); while (($row = rg_sql_fetch_array($res))) { $ki = rg_keys_info($row['key']); if ($ki['ok'] != 1) { rg_internal_error("Invalid key in db!"); continue; } $t = $ki; $t['key_id'] = $row['key_id']; if ($row['itime'] == 0) $t['itime'] = "N/A"; else $t['itime'] = gmdate("Y-m-d H:i", $row['itime']); if ($row['first_use'] == 0) $t['first_use'] = "N/A"; else $t['first_use'] = gmdate("Y-m-d H:i", $row['first_use']); if (empty($row['last_ip'])) $t['last_ip'] = "N/A"; else $t['last_ip'] = $row['last_ip']; if ($row['last_use'] == 0) $t['last_use'] = "N/A"; else $t['last_use'] = gmdate("Y-m-d H:i", $row['last_use']); $t['count'] = $row['count']; $ret[] = $t; } rg_sql_free_result($res); break; } rg_log_exit(); rg_prof_end("keys_list"); return $ret; } ?>