type = $matches[1]; $this->keydata = $matches[2]; if(isset($matches[3])) { $this->comment = $matches[3]; } elseif(is_null($uid)) { $this->comment = date('Y-m-d'); } else { $this->comment = $uid.'-'.date('Y-m-d'); } $algorithm = $this->get_openssh_info(); $hash_md5 = md5(base64_decode($this->keydata)); $hash_sha256 = hash('sha256', base64_decode($this->keydata), true); $this->fingerprint_md5 = rtrim(chunk_split($hash_md5, 2, ':'), ':'); $this->fingerprint_sha256 = rtrim(base64_encode($hash_sha256), '='); $this->randomart_md5 = $this->generate_randomart($hash_md5, "{$algorithm} {$this->keysize}", 'MD5'); $this->randomart_sha256 = $this->generate_randomart(bin2hex($hash_sha256), "{$algorithm} {$this->keysize}", 'SHA256'); if($this->keysize < $minbits && !$force) { throw new InvalidArgumentException("Insufficient bits in public key"); } } /** * Determine the algorithm and keysize of a key by passing it to OpenSSH's ssh-keygen utility. * @return string algorithm in use */ public function get_openssh_info() { $filename = tempnam('/tmp', 'key-test-'); $file = fopen($filename, 'w'); fwrite($file, $this->export()); fclose($file); exec('/usr/bin/ssh-keygen -lf '.escapeshellarg($filename).' 2>/dev/null', $output); unlink($filename); if(count($output) == 1 && preg_match('|^([0-9]+) .* \(([A-Z0-9]+)\)$|', $output[0], $matches)) { $this->keysize = intval($matches[1]); return $matches[2]; } else { throw new InvalidArgumentException("Public key doesn't look valid"); } } /** * Generate random art for the key in the same way that OpenSSH does * OpenSSH random art uses the 'drunken bishop' algorithm as explained at * https://pthree.org/2013/05/30/openssh-keys-and-the-drunken-bishop/ * @param string $string key hash to generate randomart of * @param string $keytype string containing text to include at the top of the randomart * @param string $algo string containing text to include at the bottom of the randomart * @return string containing generated randomart */ function generate_randomart($string, $keytype, $algo) { // Basic constants $max_x = 16; // Map size, x dimension $max_y = 8; // Map size, y dimension $s_x = 8; // Starting position, x coord $s_y = 4; // Starting position, y coord // Character mapping $char_map = array(' ', '.', 'o', '+', '=', '*', 'B', 'O', 'X', '@', '%', '&', '#', '/', '^'); // Build empty map $map = array(); for($x = 0; $x <= $max_x; $x++) { $map[$x] = array(); for($y = 0; $y <= $max_y; $y++) { $map[$x][$y] = 0; } } // Set the bishop to his starting position $b_x = $s_x; // Bishop position, x coord $b_y = $s_y; // Bishop position, y coord // Let him wander $chunks = str_split($string, 2); foreach($chunks as $chunk) { $binary = str_pad(base_convert($chunk, 16, 2), 8, '0', STR_PAD_LEFT); foreach(array_reverse(str_split($binary, 2)) as $bit_pair) { // Work out which diagonal direction he will move based on the bit pair $dx = ($bit_pair[1] == 0 ? -1 : 1); $dy = ($bit_pair[0] == 0 ? -1 : 1); $b_x += $dx; $b_y += $dy; // Stop him wandering outside the map $b_x = min(max($b_x, 0), 16); $b_y = min(max($b_y, 0), 8); // Increment count at his new position $map[$b_x][$b_y]++; } } // Output his path within the map $output = "+".str_pad('['.$keytype.']', $max_x + 1, '-', STR_PAD_BOTH)."+\n"; for($y = 0; $y <= $max_y; $y++) { $output .= "|"; for($x = 0; $x <= $max_x; $x++) { if($x == $b_x && $y == $b_y) { // End position $output .= 'E'; } elseif($x == $s_x && $y == $s_y) { // Start position $output .= 'S'; } else { // Output character corresponding to number of passes if(isset($char_map[$map[$x][$y]])) { $output .= $char_map[$map[$x][$y]]; } else { $output .= '^'; } } } $output .= "|\n"; } $output .= "+".str_pad('['.$algo.']', $max_x + 1, '-', STR_PAD_BOTH)."+"; return $output; } /** * Provide the key in OpenSSH-text-format. * @return string key in OpenSSH-text-format */ public function export() { return "{$this->type} {$this->keydata} {$this->comment}"; } /** * Provide a text summary of details about the key, including hashes, randomart and link to view it. * @return string text summary */ public function summarize_key_information() { global $config; $url = $config['web']['baseurl'].'/pubkeys/'.urlencode($this->id); $output = "The key fingerprint is:\n"; $output .= " MD5:{$this->fingerprint_md5}\n"; $output .= " SHA256:{$this->fingerprint_sha256}\n\n"; $output .= "The key randomart is:\n"; $randomart_md5 = explode("\n", $this->randomart_md5); $randomart_sha256 = explode("\n", $this->randomart_sha256); foreach($randomart_md5 as $ref => $line) { $output .= $line.' '.$randomart_sha256[$ref]."\n"; } $output .= "\nYou can also view the key at <$url>"; return $output; } /** * Add a GPG signature for this public key. * @param PublicKeySignature $sig GPG signature to add */ public function add_signature(PublicKeySignature $sig) { if(is_null($this->id)) throw new BadMethodCallException('Public key must be in directory before signatures can be added'); $sig->validate(); $stmt = $this->database->prepare("INSERT INTO public_key_signature SET public_key_id = ?, signature = ?, upload_date = UTC_TIMESTAMP(), fingerprint = ?, sign_date = ?"); $stmt->bind_param('dsss', $this->id, $sig->signature, $sig->fingerprint, $sig->sign_date); $stmt->execute(); $sig->id = $stmt->insert_id; $stmt->close(); $this->owner->sync_remote_access(); } /** * Delete a GPG signature for this public key. * @param PublicKeySignature $sig GPG signature to remove */ public function delete_signature(PublicKeySignature $sig) { if(is_null($this->id)) throw new BadMethodCallException('Public key must be in directory before signatures can be deleted'); $stmt = $this->database->prepare("DELETE FROM public_key_signature WHERE public_key_id = ? AND id = ?"); $stmt->bind_param('dd', $this->id, $sig->id); $stmt->execute(); $stmt->close(); $this->owner->sync_remote_access(); } /** * List all GPG signatures stored for this public key. * @return array of PublicKeySignature objects */ public function list_signatures() { if(is_null($this->entity_id)) throw new BadMethodCallException('Public key must be in directory before signatures can be listed'); $stmt = $this->database->prepare("SELECT * FROM public_key_signature WHERE public_key_id = ?"); $stmt->bind_param('d', $this->id); $stmt->execute(); $result = $stmt->get_result(); $sigs = array(); while($row = $result->fetch_assoc()) { $sig = new PublicKeySignature($row['id'], $row); $sig->public_key = $this; $sigs[] = $sig; } $stmt->close(); return $sigs; } /** * Add a destination rule specifying where this key is allowed to be synced to. * @param PublicKeyDestRule $rule destination rule to be added */ public function add_destination_rule(PublicKeyDestRule $rule) { if(is_null($this->id)) throw new BadMethodCallException('Public key must be in directory before destination rules can be added'); $stmt = $this->database->prepare("INSERT INTO public_key_dest_rule SET public_key_id = ?, account_name_filter = ?, hostname_filter = ?"); $stmt->bind_param('dss', $this->id, $rule->account_name_filter, $rule->hostname_filter); $stmt->execute(); $rule->id = $stmt->insert_id; $stmt->close(); $this->owner->sync_remote_access(); } /** * Delete a destination rule that specified where this key was allowed to be synced to. * @param PublicKeyDestRule $rule destination rule to be removed */ public function delete_destination_rule(PublicKeyDestRule $rule) { if(is_null($this->id)) throw new BadMethodCallException('Public key must be in directory before destination rules can be added'); $stmt = $this->database->prepare("DELETE FROM public_key_dest_rule WHERE public_key_id = ? AND id = ?"); $stmt->bind_param('dd', $this->id, $rule->id); $stmt->execute(); $stmt->close(); $this->owner->sync_remote_access(); } /** * List all destination rule currently applying to this key. * @return array of PublicKeyDestRule objects */ public function list_destination_rules() { if(is_null($this->entity_id)) throw new BadMethodCallException('Public key must be in directory before destination rules can be listed'); $stmt = $this->database->prepare("SELECT * FROM public_key_dest_rule WHERE public_key_id = ?"); $stmt->bind_param('d', $this->id); $stmt->execute(); $result = $stmt->get_result(); $rules = array(); while($row = $result->fetch_assoc()) { $rules[] = new PublicKeyDestRule($row['id'], $row); } $stmt->close(); return $rules; } }