view marcus.php @ 4:c642254dc9ee draft default tip

Fixed transfer chain generation and construction of empty updates, some small improvements in tools
author Ivo Smits <Ivo@UCIS.nl>
date Sat, 22 Nov 2014 18:18:52 +0100
parents 5c8c4fa95803
children
line wrap: on
line source

<?php
require_once './marccore.php';

error_reporting(E_ALL);

if (!isset($argv)) $argv = $_SERVER['argv'];
$argi = 1;

class FilteredResourceIterator implements Iterator {
	private $source, $filtertype, $filtervalue;
	public function __construct($source, $filtertype, $filtervalue) {
		$this->source = $source;
		$this->filtertype = $filtertype;
		$this->filtervalue = $filtervalue;
	}
	public function current() { $r = $this->source->current(); return $r; }
	public function key() { return $this->source->key(); }
	public function valid() { return $this->source->valid(); }
	public function next() { $this->source->next(); $this->findnext(); }
	public function rewind() { $this->source->rewind(); $this->findnext(); }
	private function findnext() {
		while ($this->source->valid()) {
			$c = $this->source->current();
			if ($this->filter($c)) break;
			$this->source->next();
		}
	}
	private function filter($c) {
		switch ($this->filtertype) {
			case 'OWNER': return isset($c['value']['owner']) && is_scalar($c['value']['owner']) && strcasecmp($c['value']['owner'], $this->filtervalue) == 0;
			case 'KEY': return $c['key'] == hex2bin($this->filtervalue);
			case 'DOMEXT': return ord($c['label'][0]) == 4 && substr_compare($c['label'], $this->filtervalue, -strlen($this->filtervalue), strlen($this->filtervalue), TRUE) == 0;
			default: return FALSE;
		}
	}
}

$database = new MARCDatabaseFlatFile();
$key = NULL;
$resource = NULL;
$reschanged = FALSE;

while ($argi < count($argv)) {
	switch (strtoupper($argv[$argi++])) {
		case 'OPEN':
			if ($reschanged) echo "Warning: selected resource has not been updated.\n";
			if ($database->IsChanged()) echo "Warning: database has unsaved changes.\n";
			$database->Open($argv[$argi++]);
			$reschanged = FALSE;
			break;
		case 'OPENSQLITE':
			$database = new MARCDatabaseSQLite($argv[$argi++]);
			break;
		case 'OPENDBA':
			$database = new MARCDatabaseDBA($argv[$argi++]);
			break;
		case 'SAVE':
			$database->Save();
			break;
		case 'SAVEAS':
			$database->SaveAs($argv[$argi]);
			break;
		case 'SYNC':
			$database->SyncHTTP($argv[$argi++]);
			break;
		case 'KEY':
			switch (strtoupper($argv[$argi++])) {
				case 'CREATE':
					$key = array('store' => TRUE);
					$key['pk'] = nacl_crypto_sign_ed25519_keypair($key['sk'], randombytes(32));
					echo 'Created public key '.bin2hex($key['pk'])."\n";
					break;
				case 'FORGET':
					$key['store'] = FALSE;
					break;
				case 'STORE':
					$key['store'] = TRUE;
					break;
				case 'USE':
					$key = array('store' => FALSE, 'pk' => $resource['key']);
					if (isset($resource['value']['seckey'])) $key['locked'] = $resource['value']['seckey'];
					break;
				case 'IMPORT':
					$key = array('store' => FALSE);
					$key['pk'] = nacl_crypto_sign_ed25519_keypair($key['sk'], hex2bin($argv[$argi++]));
					break;
				case 'UNLOCK':
					if (!isset($key['locked'])) throw new Exception('The key is not locked');
					if (!is_array($key['locked']) || !isset($key['locked']['key'])) throw new Exception('The locked key is invalid');
					$rounds = isset($key['locked']['rounds']) ? intval($key['locked']['rounds']) : 0;
					$ret = str_repeat(chr(0), 64);
					for ($i = 0; $i < $rounds; $i++) $ret = hash('sha512', $ret.$argv[$argi].$key['pk'], TRUE);
					$argi++;
					$ret = substr($key['locked'] ^ $ret, 0, 32);
					$ret = nacl_crypto_sign_ed25519_keypair($key['sk'], $ret);
					if ($ret != $key['pk']) throw new Exception('Key password is not valid');
					echo 'Unlocked public key '.bin2hex($key['pk'])."\n";
					break;
				default:
					throw new Exception('Unknown key operation '.$argv[$argi-1]);
			}
			break;
		case 'LIST':
			foreach ($database->GetResources() as $ret) echo labeltoname($ret['label'])."\n";
			break;
		case 'FIND':
			foreach (filterresources($database->GetResources(), $argv, $argi) as $ret) echo labeltoname($ret['label'])."\n";
			break;
		case 'DELETE':
			$database->DeleteResource($resource['label']);
			break;
		case 'DUMP':
			dumpresource($resource);
			break;
		case 'UPDATE':
			if (!isset($resource['key'])) $resource['key'] = $key['pk'];
			unset($resource['serial']);
			$res = $database->UpdateResource($resource, $key['sk']);
			if (!$res) throw new Exception('Could not update resource');
			$resource = $res->ToArray();
			$reschanged = FALSE;
			break;
		case 'SET':
			switch (strtoupper($argv[$argi++])) {
				case 'OWNER':
					if (!isset($resource['value']) || !is_array($resource['value'])) $resource['value'] = array();
					$resource['value']['owner'] = $argv[$argi++];
					$reschanged = TRUE;
					break;
				case 'DESC':
				case 'DESCR':
				case 'DESCRIPTION':
					if (!isset($resource['value']) || !is_array($resource['value'])) $resource['value'] = array();
					$resource['value']['descr'] = $argv[$argi++];
					$reschanged = TRUE;
					break;
				case 'PWAUTH':
					if (!isset($key['sk'])) throw new Exception('The key is not available');
					$rounds = 5000;
					$ret = str_repeat(chr(0), 64);
					for ($i = 0; $i < $rounds; $i++) $ret = hash('sha512', $ret.$argv[$argi].$key['pk'], TRUE);
					$argi++;
					$key['locked'] = array('rounds' => $rounds, 'key' => substr($key['sk'] ^ $hash, 0, 32));
					if (!isset($resource['value']) || !is_array($resource['value'])) $resource['value'] = array();
					$resource['value']['seckeyenc'] = $key['locked'];
					$reschanged = TRUE;
					break;
				case 'TRANSFER':
					$ret = $argv[$argi++];
					$resource['value']['transfer'] = (strtolower($ret) == 'any') ? '' : hex2bin($ret);
					$reschanged = TRUE;
					break;
				default:
					throw new Exception('Unknown set operation '.$argv[$argi-1]);
			}
			break;
		case 'ADD':
			switch (strtoupper($argv[$argi++])) {
				case 'NS':
					if (!isset($resource['value']) || !is_array($resource['value'])) $resource['value'] = array();
					if (!isset($resource['value']['ns']) || !is_array($resource['value']['ns'])) $resource['value']['ns'] = array();
					$nsname = $argv[$argi++];
					$nsglue = (strlen($nsname) && $nsname[strlen($nsname)-1] != '.') ? $argv[$argi++] : NULL;
					if (!isset($resource['value']['ns'][$nsname]) || !is_array($resource['value']['ns'][$nsname])) $resource['value']['ns'][$nsname] = array();
					if ($nsglue !== NULL) $resource['value']['ns'][$nsname][] = $nsglue;
					$reschanged = TRUE;
					break;
				default:
					throw new Exception('Unknown add operation '.$argv[$argi-1]);
			}
			break;
		case 'RESET':
			switch (strtoupper($argv[$argi++])) {
				case 'OWNER':
					if (!is_array($resource['value'])) $resource['value'] = array();
					unset($resource['value']['owner']);
					$reschanged = TRUE;
					break;
				case 'DESC':
				case 'DESCR':
				case 'DESCRIPTION':
					if (!is_array($resource['value'])) $resource['value'] = array();
					unset($resource['value']['descr']);
					$reschanged = TRUE;
					break;
				case 'PWAUTH':
					if (!is_array($resource['value'])) $resource['value'] = array();
					unset($resource['value']['seckeyenc']);
					$reschanged = TRUE;
					break;
				case 'NS':
					if (!is_array($resource['value'])) $resource['value'] = array();
					unset($resource['value']['ns']);
					$reschanged = TRUE;
					break;
				case 'VALUE':
					$resource['value'] = array();
					$reschanged = TRUE;
					break;
				case 'TRANSFER':
					unset($resource['transfer']);
					$reschanged = TRUE;
					break;
				case 'EXPIRATION':
					unset($resource['expiration']);
					$reschanged = TRUE;
					break;
				default:
					throw new Exception('Unknown reset operation '.$argv[$argi-1]);
			}
			break;
		case 'CREATE':
			if ($reschanged) echo "Warning: selected resource has not been updated.\n";
			$reschanged = TRUE;
			$resource = array('label' => argtolabel($argv, $argi));
			break;
		case 'SELECT':
			if ($reschanged) echo "Warning: selected resource has not been updated.\n";
			$reschanged = FALSE;
			if (strcasecmp($argv[$argi], 'TRANSFERCHAIN') == 0) {
				$resource = MARCUpdate::Decode($resource['transferchain']);
				if (!$resource) echo "Warning: failed to decode chained transfer data.\n";
				$argi++;
			} else {
				$label = argtolabel($argv, $argi);
				$resource = $database->GetResource($label);
				if (!$resource) echo "Warning: resource ".labeltoname($label)." does not exist.\n";
			}
			if ($resource) $resource = $resource->ToArray();
			break;
		case 'HELP':
			print_help();
			break;
		default:
			throw new Exception('Unknown operation '.$argv[$argi-1]);
	}
}
if ($reschanged) echo "Warning: selected resource has not been updated.\n";
if ($database->IsChanged()) echo "Warning: database has unsaved changes.\n";
$database->Close();

function filterresources($iterator, $argv, &$argi) {
	$filtertype = strtoupper($argv[$argi++]);
	switch ($filtertype) {
		case 'OWNER':
		case 'KEY':
		case 'DOMEXT':
			$filtervalue = $argv[$argi++];
			break;
		default:
			throw new Exception('Unknown filter type '.$t);
	}
	return new FilteredResourceIterator($iterator, $filtertype, $filtervalue);
}
function argtolabel($argv, &$argi) {
	$t = $argv[$argi++];
	switch (strtoupper($t)) {
		case 'LABEL': return hex2bin($argv[$argi++]);
		case 'CURRENTKEY': return chr(0).$GLOBALS['key']['pk'];
		case 'RESOURCEKEY': return chr(0).$GLOBALS['resource']['key'];
		case 'KEY': return chr(0).hex2bin($argv[$argi++]);
		case 'IP':
		case 'IP4':
		case 'IPV4':
		case 'IP6':
		case 'IPV6': return ipnettolabel($argv[$argi++]);
		case 'AS': return chr(3).marc_decode_int32be($argv[$argi++]);
		case 'DOM':
		case 'DOMAIN': return chr(4).strtolower(trim($argv[$argi++], '.'));
		default:
			if (preg_match('/^AS[0-9]{1-9}$/', $t)) return chr(3).marc_decode_int32be(substr($t, 2));
			if (preg_match('_^[0-9]{1-3}\.[0-9]{1-3}\.[0-9]{1-3}\.[0-9]{1-3}/[0-9]{1-2}$_', $t)) return ipnettolabel($t);
			if (preg_match('_^(((?=.*(::))(?!.*\3.+\3))\3?|([\dA-F]{1,4}(\3|:\b|$)|\2))(?4){5}((?4){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})\z/[0-9]{1-3}_i', $t)) return ipnettolabel($t);
			if (preg_match('/^[a-f0-9]{64}$/i', $t)) return chr(0).hex2bin($t);
			if (preg_match('/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z0-9]{2,6}$/i', $t)) return chr(4).strtolower(trim($t, '.'));
			throw new Exception('Could not detect label type for '.$t);
	}
}
function ipnettolabel($s) {
	$ip = inet_pton(strtok($s, '/'));
	$pl = intval(strtok('/'));
	if ($pl == 0) throw new Exception('Invalid IP network specified');
	if (strlen($ip) == 4) return chr(1).$ip.chr($pl);
	if (strlen($ip) == 16) return chr(2).$ip.chr($pl);
}
function labeltoname($l) {
	switch (ord($l)) {
		case 0: return 'KEY '.bin2hex(substr($l, 1));
		case 1: if (strlen($l) == 6) return 'IPv4 '.inet_ntop(substr($l, 1, 4)).'/'.ord($l[5]); else return 'LABEL '.bin2hex($l);
		case 2: if (strlen($l) == 18) return 'IPv6 '.inet_ntop(substr($l, 1, 16)).'/'.ord($l[17]); else return 'LABEL '.bin2hex($l);
		case 3: if (strlen($l) == 5) return 'AS '.marc_decode_int32be(substr($l, 1, 4)); else return 'LABEL '.bin2hex($l);
		case 4: if (strlen($l) > 1) return 'DOM '.substr($l, 1); else return 'LABEL '.bin2hex($l);
		default: return 'LABEL '.bin2hex($l);
	}
}
function dumpresource($r, $p = '') {
	if (is_null($r)) {
		echo "NULL\n";
	} else if (is_string($r)) {
		$bin = FALSE;
		for ($i = 0; $i < strlen($r); $i++) $bin |= ord($r[$i]) < 32 || ord($r[$i]) > 126;
		if ($bin) echo '0x'.bin2hex($r)."\n";
		else echo '"'.$r.'"'."\n";
	} else if (is_scalar($r)) {
		echo $r."\n";
	} else if (is_array($r)) {
		echo "array(\n";
		foreach ($r as $key => $value) {
			echo $p.'    ['.$key.'] => ';
			dumpresource($value, $p.'    ');
		}
		echo $p."  )\n";
	} else {
		print_r($r);
	}
}
function randombytes($n) {
	$b = '';
	$file = fopen('/dev/urandom', 'r');
	for ($i = 0; $i < $n; $i++) $b .= fgetc($file);
	fclose($file);
	return $b;
}

function print_help() {
	echo 'Usage: marcus.php [operation] [arguments] [operation] [arguments]...
open [filename.mdb] - opens the specified database file
save - saves the current database file
saveas [filename.mdb] - saves the current data to a new databse file
sync [url] - synchronize database with remote server
key create - create a new key pair
key forget - do not store the current key pair in the local database
key store - store the current key pair in the local database
key use - use the key pair from the currently selected resource
key import [secretkey] - import the key pair defined by the given secret key
key unlock [password] - unlock a password protected key pair
list - list registered resources
find [type] [value] - list registered sources matching filter (type=OWNER|KEY|DOMEXT)
create [identifier] - create given resource
create currentkey - create resource for current key pair
create ip|ip4|ipv4 [ipv4network] - create resource for IPv4 network
create ip|ip6|ipv6 [ipv6network] - create resource for IPv6 network
create dom|domain [ipv6network] - create resource for domain name
select [identifier] - select resource given by identifier
select currentkey - select key resource for current key pair
select resourcekey - select key resource for the key that signed the currently selected resource
select transferchain - extract the chained transfer data from the currently selected resource
select label [identifier] - select resource by hexadecimal label
select key [publickey] - select key resource (hexadecimal)
select ip|ip4|ipv4 [ipv4network] - select resource for IPv4 network
select ip|ip6|ipv6 [ipv6network] - select resource for IPv6 network
select dom|domain [ipv6network] - select resource for domain name
delete - delete currently selected resource
dump - display currently selected resource
update - update selected resource in the local database
set owner [name] - set the owner name for the selected resource
set descr|desc|description [text] - set the description for the selected resource
set pwauth [password] - store the current key pair in the selected resource for password authentication
set transfer any - allow anyone to take over the resource
set transfer [key] - transfer the resource to given key
TODO: set expiration [value]
add ns [name] [glue] - add in-zone nameserver with glue record (name is the part of the nameserver name before the domain name, glue is an IPv4 or IPv6 address)
add ns [name]. - add an external nameserver
reset owner - clear the owner
reset descr|desc|description - clear the description
reset pwauth - remove the key pair from the recourse, disabling password authentication
reset ns - clear the nameserver records
reset transfer - disable resource transfers
reset expiration - disable explicit expiration
reset value - clear the owner, description, password authentication and nameserver records

Examples:
OPEN marc.mdb KEY create CREATE currentkey SET owner "Your name" SET pwauth yourpassword UPDATE KEY forget CREATE yourdomain.ano SET owner "Your name" UPDATE SAVE
OPEN marc.mdb SELECT yourdomain.ano SELECT resourcekey KEY use KEY unlock yourpassword SELECT yourdomain.ano ADD ns ns1 1.2.3.4 UPDATE SAVE
OPEN marc.mdb SELECT yourdomain.ano SELECT resourcekey KEY use KEY unlock yourpassword CREATE 1.2.3.0/24 SET owner "Your name" ADD ns ns1.yourdomain.ano. UPDATE SAVE
OPEN marc.mdb SYNC http://marc.ucis.ano/ SAVE
';
}