File: include/KeePassWriter.inc.php

Recommend this page to a friend!
  Classes of Colin McKinnon  >  PHP KeePassX Writer  >  include/KeePassWriter.inc.php  >  Download  
File: include/KeePassWriter.inc.php
Role: Class source
Content type: text/plain
Description: Class source
Class: PHP KeePassX Writer
Create database of passwords for KeePass
Author: By
Last change:
Date: 4 months ago
Size: 11,207 bytes
 

 

Contents

Class file image Download
<?php
/**
 * @Project kxc-php
 * @author colin.mckinnon
 *
 * KeePassWriter provides an API wrapper around the
 * Keepassxc-cli binary, allowing the creation
 * of a KeePass database.
 *
 * No data is committed to the local storage unencrypted (unless in swap)
 */
require_once('kpx_icons.inc.php'); // define contstants to describe icons

class KeePassWriter {
	private $filename;
	private $passphrase;
	private $data;
	private $exec; // path to keepassxc-cli binary
	private $timeout;
	public  $keepass_error; // will contain any errors reported by the keepassxc-cli runtime
	/**
	 * param string $filename path to write database to (dirs will created if permitted) MUST NOT EXIST!
	 * param string $passphrase passphrase used to encrypt the data
	 *
	 * Permform some sanity checks and prepare the stub data
	 */
	function __construct($filename, $passphrase, $timeout=30, $exec=false)
	{
		$this->changeParams($filename, $passphrase);
		if ($exec && !is_executable($exec)) {
			trigger_error("Supplied path for keepassxc-cli is not executable", E_USER_ERROR);
		}
		if (!$exec) {
			$exec="keepassxc-cli";
		}
		if (!function_exists('posix_mkfifo')) {
			trigger_error("KeePassWriter requires the POSIX extension", E_USER_ERROR);
		}
		if ((integer)$timeout) {
			$this->timeout=$timeout;
		} else {
			$this->timeout=20;
		}
		$this->exec=$exec;
		$this->keepass_error="Not yet invoked";
		$this->Data=array('Name'=>'Root', 'IconID'=>KPX_ICON_DEFAULT, 'Notes'=>'', 'g'=>array(), 'e'=>array());
	}
	/**
	 * @param string $filename - set the path for the new database
	 * @param string $passphrase - set the passphrase for the new database
	 *
	 * It's a rather involved process populating the dataset. This method is here to simplify
	 * the creation of multiple instances with different passphrases without
	 * having to regenerate the data structure
	 */
	public function changeParams($filename, $passphrase)
	{
		if (file_exists($filename)) {
                        trigger_error("File already exists", E_USER_ERROR);
                }
		$path=realpath(dirname($filename));
                if (!is_dir($path) && !mkdir($path, true)) {
                        trigger_error("Path does not exist / cannot be created", E_USER_ERROR);
                }
		$this->filename=$filename;
		$this->passphrase=$passphrase;
	}
	/**
	 * @param resource $handle Open file handle to write data to
	 *
	 * Normally this should not be called directly but is exposed as a public
	 * function for debugging purposes ( $kpx->writedata(STDOUT); )
	 */
	public function writedata($handle)
	{
		fputs($handle, "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
		fputs($handle, "<KeePassFile>\n<Root>\n");
		$this->writegroups($handle, $this->data['Root']);
		fputs($handle, "</Root>\n</KeePassFile>\n");
	}
	private function writegroups($handle, $arr)
	{
		fputs($handle, "<Group>\n<Name>" 
                    . htmlspecialchars($arr['Name'], ENT_XML1, 'UTF-8')
		    . "</Name>");
		if (isset($arr['Notes'])) {
			fputs($handle, "<Notes>"
			 . htmlspecialchars($arr['Notes'], ENT_XML1, 'UTF-8')
			 . "</Notes>\n");
		}
		if (!isset($arr['IconID']) || !(integer)$arr['IconID']) {
			$arr['IconID']=KPX_ICON_DEFAULT; // Default folder
		}
		fputs($handle, "<IconID>" . (integer)$arr['IconID'] . "</IconID>\n");
		foreach($arr['g'] as $subgroup) {
			$this->writegroups($handle, $subgroup);
		}
		foreach($arr['e'] as $entry) {
			fputs($handle, "<Entry>\n$entry\n</Entry>\n");
		}
		fputs($handle, "</Group>\n");
		
	}
	/**
	 * param string $path Hierarchy of groups written as a file path e.g. infrastructure/switches/Cisco
	 * param string $notes Any runtime supplied description of the group
	 * param integer $icon id for one of the Keepassxc icons
	 *
	 * Keepass "Groups" are folders - analogous to directories on a filesystem
	 * At the top level, Keepass has a group named "Root" but this is automatically added by this lib
	 */
	public function addgroup($path, $notes, $icon=false)
	{
		$parts=explode("/", "Root/" . trim($path, " /\r\n"));
		$this->buildpath($parts, $this->data, $notes, $icon);
	}
	/**
	 * Recursive slave function to addgroup() method
	 */
	private function buildpath($parts, &$arr, $notes, $icon) 
	{
		$part=array_shift($parts);
		if (!isset($arr[$part])) {
			$arr[$part]=array('Name'=>$part, 'e'=>array(), 'g'=>array());
		}
		if (count($parts)) {
			$this->buildpath($parts, $arr[$part]['g'], $notes, $icon);
		} else {
			$arr[$part]['Notes']=$notes;
			$arr[$part]['IconID']=$icon;
		}
	}
	/**
	 * param string $path See "addgroup" method
	 * param string $title The name for the secret record
	 * param string $username The account name on the target
	 * param string $secret The authentication token (usually a password) for the target
	 * param string $url The URL for the target
	 * param string $notes Any runtime supplied description of the record
	 *
	 * Creates the XML for a secret record and places it in the specified group hierarchy
	 */
	public function additem($path, $title, $username, $secret, $url, $notes)
	{
		$item="<String><Key>Title</Key><Value>" 
		. htmlspecialchars($title, ENT_XML1, 'UTF-8') 
		. "</Value></String>\n"
		. "<String><Key>UserName</Key><Value>"
		. htmlspecialchars($username, ENT_XML1, 'UTF-8')
		. "</Value></String>\n"
		. "<String><Key>Password</Key><Value ProtectInMemory=\"True\">"
		. htmlspecialchars($secret, ENT_XML1, 'UTF-8')
		. "</Value></String>\n"
		. "<String><Key>URL</Key><Value>"
                . htmlspecialchars($url, ENT_XML1, 'UTF-8')
                . "</Value></String>\n"
		. "<String><Key>Notes</Key><Value>"
                . htmlspecialchars($notes, ENT_XML1, 'UTF-8')
                . "</Value></String>";
		$parts=explode("/", "Root/" . trim($path, " /\r\n"));
		$this->builditem($parts, $this->data, $item);

	}
	/**
	 * Recursive slave function to additem method
	 */
	private function builditem($pathparts, &$arr, $item)
	{
		$pathpart=array_shift($pathparts);
		if (!isset($arr[$pathpart]) || !is_array($arr[$pathpart])) {
			$arr[$pathpart]=array('Name'=>$pathpart, 'g'=>array(), 'e'=>array());
		}
		if (count($pathparts)) {
			$this->builditem($pathparts, $arr[$pathpart]['g'], $item);
		} else {
			$arr[$pathpart]['e'][]=$item;
		}
	}
	/**
	 * Create a Keepass data using the previously supplied data
	 *
	 * this gets complicated due to the fact that opening a FIFO for writing
	 * blocks until a reader also opens the file.
	 * To deal with this, the code calls pcntl_fork()
	 * - the child opens the FIFO, sends the data in and exits
	 * - the original process starts keepassxc-cli and sends it the passphrase
	 *   then waits to see how keepassxc-cli responds
	 * Hopefully the OS memory COW means that we don't double the memory usage!
	 */
	function createdb()
	{
		$tmpfile=$this->mkfifo();
		// print "assigned filename $tmpfile\n";
		// print "Master process is " . getmypid() . "\n";
		$pid=pcntl_fork();
		if (-1 == $pid) {
			trigger_error("Failed to fork", E_USER_ERROR);
		}
		if (0==$pid) {
			// this is the spawned process
			// which will write to the fifo
			// print "Writer process is " . getmypid() . "\n";
			$this->datasender($tmpfile);
			exit(0);
		} else {
			// this is the controlling process
			$result=$this->controlslaves($tmpfile,$pid);
		}
		unlink($tmpfile);
		return $result;
	}
	/**
	 * @param string $tmpfile path+name of fifo 
	 * 
	 * Invoked in the child process ONLY
	 */
	private function datasender($tmpfile)
	{
		if (function_exists('pcntl_async_signals')) {
			pcntl_async_signals(true);
		}
		pcntl_signal(SIGALRM, array($this, 'timeout'));
		pcntl_alarm($this->timeout); // Note, only set in child
		$fifo=fopen($tmpfile, "w"); // this blocks until fifo also open for reading
					// hence earlier pcntl_fork()
                // print "opened fifo for write\n";
                if (!is_resource($fifo)) {
                        trigger_error("Failed to open fifo for write", E_USER_ERROR);
                }
                // print "fifo opened\n";
		$this->writedata($fifo);
                // print "data sent\n";
		fclose($fifo);
		// print "datasender has closed fifo\n";
	}
	/**
	 * in case child gets blocked indefinitely....
	 */
	function timeout()
	{
		exit(1);
	}
	/**
	 * @param string $tmpfile path+name of fifo
	 * @param string $pid process id of forked (child) instance
	 * @return bool true if database created
	 *
	 * invoked in  the parent process ONLY
	 */
	private function controlslaves($tmpfile, $pid)
	{
		$io=array();
                $iodef=array(
                        0 => array('pipe', 'r'),
                        1 => array('pipe', 'w'),
                        2 => array('pipe', 'w'));
                $cmd=$this->exec . " import " . escapeshellarg($tmpfile) . " " . escapeshellarg($this->filename);
                $proc=proc_open($cmd, $iodef, $io, sys_get_temp_dir());
                if (!is_resource($proc)) {
                        trigger_error("Failed to invoke executable");
                }
		$this->keepass_error="KeePass import process invoked";
                $this->setpassphrase($io);
                // print "Key set\n";
		// $cmd will now open the fifo and start reading from it.
		// we wait for the sender to finish....
		$status=0;
		pcntl_waitpid($pid, $status);
		if (!pcntl_wifexited($status)) { // did it fail to exit cleanly?
			trigger_error("Forked instance did not exit cleanly", E_USER_WARNING);
			$result=false;
		} else {
			$result=true;
		}
		$response=stream_get_contents($io[1]);
                $this->keepass_error=trim(stream_get_contents($io[2]));
		if (!strstr($response, 'Successfully imported database')) {
			$result=false;
		}
                fclose($io[1]);
                fclose($io[2]);
                fclose($io[0]);
                // print "response=$response\n====\nerr=$err_response\n";
		return $result;
	}
	/**
	 * Generate a unique filename for the FIFO
	 * (to mitigate but not eliminate MITM)
	 */
	private function mkfifo()
	{
		$tmpfile=tempnam(sys_get_temp_dir(), "KPX");
                if (!$tmpfile) {
                        trigger_error("Failed to create pipe file", E_USER_ERROR);
                }
                unlink($tmpfile);
                if (!posix_mkfifo($tmpfile, 0600)) {
                        trigger_error("Failed to create fifo", E_USER_ERROR);
                }
		return $tmpfile;	
	}
	/**
	 * @param array $io the 3 file handles created by proc_open
	 */
	private function setpassphrase($io)
	{
		$prompt='';

		while (!feof($io[2])) {
			$c=fgetc($io[2]);
			fputs(STDOUT, $c);
			$prompt.=$c;
			if (':'==$c) {
				break;
			}
		}
		if (feof($io[2]) || $prompt!='Enter password to encrypt database (optional):') {
			trigger_error("Unexpected prompt from executable " . base64_encode($prompt), E_USER_ERROR);
		}
		fputs($io[0], $this->passphrase . "\n");
		$prompt='';
		while (!feof($io[2])) {
			$c=fgetc($io[2]);
			$prompt.=$c;
			if (':'==$c) {
				break;
			}
		}
		$prompt=trim($prompt);
		if (feof($io[2]) || $prompt!='Repeat password:') {
			trigger_error("Unexpected second prompt from executable " . base64_encode($prompt), E_USER_ERROR);
		}
		fputs($io[0], $this->passphrase . "\n");
	}
}

For more information send a message to info at phpclasses dot org.