#!/usr/bin/perl -I/usr/local/sauron
#
# keygen -- utility for creating TSIG/DNSSEC keys
#
# Copyright (c) Timo Kokkonen <tjko@iki.fi>  2005.
# $Id: keygen,v 1.4 2005/01/28 08:22:07 tjko Exp $
#
require 5;
use Getopt::Long;
use Sauron::DB;
use Sauron::Util;
use Sauron::Sauron;
use Sauron::BackEnd;
use Digest::MD5;
use MIME::Base64;
use Crypt::RC5;

my %KEY_ALGORITHMS = ( 
		       0=>'reserved',
		       1=>'RSA/MD5',
		       2=>'DH',
		       3=>'DSA',
		       4=>'ECC',
		       157=>'HMAC-MD5'
		      );

load_config();

GetOptions("help|h","setmasterkey:s","add:s","del:s","regen:s","list:s",
	   "verbose|v");

if ($opt_help || not (defined($opt_add) || defined $opt_del || 
		   defined $opt_regen || defined $opt_list || 
		   defined $opt_setmasterkey) ) {
  print "syntax: $0 <server> [OPTIONS]\n",
        "interactive options:\n\n",
        "\t--setmasterkey\tset sauron master key\n",
        "\t--add\t\tadd a key\n",
        "\t--del\t\tdelete a key\n",
        "\t--regen\t\tregenerate (non-static) key(s)\n",
        "\t--list\t\tlist keys\n\n",
        "non-interactive options:\n\n",
        "\t--setmasterkey=\"<passphrase>\"\n",
        "\t--list=<server>\n",
        "\t--regen=<server>,<keytype>\n",
        "\t--del=<server>,<keyname>\n",
        "\t--add=\"<server>,<keyname>,<keytype>,<keylen>[,<comments>,<passphrase>]\"\n",
	"\n\t--verbose\tverbose output\n",
        "\n\nsupported key types: TSIG\n\n";
  exit(0);
}

my $user = (getpwuid($<))[0];
set_muser($user);
umask 077;

db_connect();

# --setmasterkey

if (defined $opt_setmasterkey) { setmasterkey($opt_setmasterkey); }
elsif (defined $opt_add) { addkey($opt_add); }
elsif (defined $opt_del) { delkey($opt_del); }
elsif (defined $opt_list) { listkeys($opt_list); }
elsif (defined $opt_regen) { regenkeys($opt_regen); }
else { fatal("unknown option?"); }

exit;

##########################################################################


sub getserver($) {
    my($ans) = @_;
    my($ret);
    unless ($ans) {
	print "Server name: ";
	chomp($ans = <STDIN>);
    }
    $ret=get_server_id($ans);
    fatal("cannot find server: $ans") unless($ret > 0);
    return $ret;
}

sub getname($) {
    my($ans) = @_;
    unless ($ans) {
	print "Key name: ";
	chomp($ans = <STDIN>);
    }
    fatal("invalid key name: $ans") unless (valid_texthandle($ans));
    return $ans;
}

sub gettype($) {
    my($ans) = @_;
    unless ($ans) {
	print "Key type [TSIG]: ";
	chomp($ans = <STDIN>);
	$ans = 'TSIG' unless ($ans);
    }

    fatal("invalid/unsupported key type: $ans")
	unless ($ans =~ /^(TSIG)$/);
    return $ans;
}

sub getlen($$$$) {
    my($ans,$min,$max,$def) = @_;
    unless ($ans ne '') {
	print "Key length ($min-$max) [$def]: ";
	chomp($ans = <STDIN>);
    }
    $ans=$def unless ($ans);
    fatal("key site out of range ($min-$max): $ans")
	unless ($ans >= $min && $ans <= $max);
    return $ans;
}

sub make_tmpdir() {
    my $ctx = Digest::MD5->new;
    my $prefix = '/tmp/sauron.';
    my $tmpdir;

    while ($tmpdir eq '') {
	$ctx->add($$);
	$ctx->add(rand(999999999));
	$tmpdir=$prefix.$ctx->hexdigest;
	$tmpdir='' if (-e $tmpdir);
    } 

    mkdir($tmpdir,0700) || 
	fatal("failed to create temporary directory: $tmpdir");
    return $tmpdir;
}


sub generate_key($$$) {
    my($name,$type,$len) = @_;
    my $key;

    fatal("SAURON_DNSSEC_KEYGEN_PROG configuration option not defined")
	unless ($SAURON_DNSSEC_KEYGEN_PROG);
    fatal("Cannot execute: $SAURON_DNSSEC_KEYGEN_PROG")
	unless (-x $SAURON_DNSSEC_KEYGEN_PROG);

    my $opts = $SAURON_DNSSEC_KEYGEN_ARGS;

    if ($type eq 'TSIG') {
	$args = "-a HMAC-MD5 -b $len -n USER"; 
    } else {
	fatal("support this keytype not implemented yet: $type");
    }

    my $tmpdir=make_tmpdir();
    chdir($tmpdir) || fatal("cannot access tmp dir: $tmpdir");

    my $keyid = '';
    open(PIPE,"$SAURON_DNSSEC_KEYGEN_PROG $args $opts keygen |") ||
	fatal("pipe failed");
    while(<PIPE>) {
	chomp;
        $keyid=$1 if (/^(Kkeygen\S+)\s*$/);
    }
    close(PIPE);
    fatal("failed to generate key!") unless ($keyid);

    my $keyfile = $tmpdir."/".$keyid.".key";
    my $keyfile2 = $tmpdir."/".$keyid.".private";

    open(FILE,$keyfile) || fatal("failed to open: $keyfile");
    while(<FILE>) {
	chomp;
	$key = $1 if (/^\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S+)/);
    }
    close(FILE);


    unlink($keyfile) || error("failed to remove: $keyfile");
    unlink($keyfile2) || error("failed to remove: $keyfile2");
    rmdir($tmpdir) || error("failed to remove: $tmpdir");

    return $key;
}



sub setmasterkey($) {
    my($masterkey) = @_;
    my $keyfile=$CONFIG_FILE . ".key";
    my $oldkey;

    if (-f $keyfile) {
	my $fmode = (stat($keyfile))[2];
	fatal("existing keyfile with unsafe permissions: $keyfile")
	    if ($fmode & 0027);

	open(FILE,"$keyfile") || fatal("failed to open: $keyfile");
	while(<FILE>) {
	    $oldkey=$1 if (/^Key:\s+(\S+)\s*$/);
	}
	close(FILE);
	
	if ($oldkey) {
	    fatal("invalid (not Base64 encoded) masterkey in $keyfile")
		unless ($oldkey =~ /^[a-zA-Z0-9+\/=]+$/);
	    $oldkey=decode_base64($oldkey);

	    print 
		"WARNING! Backup database and config.key before attempting\n",
		"to change Sauron masterkey! Continue (N/y) ?";
	    my $cont = <STDIN>;
	    chomp($cont);
	    fatal("operation aborted") unless ($cont =~ /^[Yy]$/);
	}
    }

    unless ($masterkey) {
	print "Enter passphrase: ";
	chomp($masterkey=<STDIN>);
	fatal("Cannot use empty passphrase") unless ($masterkey);
    }
	
    my $ctx = Digest::MD5->new;
    $ctx->add($masterkey);
    $masterkey='';
    my $key = $ctx->digest;

    if ($oldkey) {
	# re-encrypt existing keys in database
	print "Re-encrypting existing keys in database...\n";
	my (@keys,$i);
	db_begin();
	if  (db_query("SELECT id,secretkey,name,algorithm " .
		      "FROM keys",\@keys) < 0) {
	    db_rollback();
	    fatal("failed to read existing keys from database");
	}
	for $i (0..$#keys) {
	    my $name = $keys[$i][2];
	    my $algo = $keys[$i][3];
	    fatal("unsupported key: $name (algorithm=$algo)") 
	      if ($algo != 157);
	    if ($keys[$i][1]) {
		my $ref1 = Crypt::RC5->new($oldkey,16);
		my $okey=$ref1->decrypt(decode_base64($keys[$i][1]));
		my $ref2 = Crypt::RC5->new($key,16);
		my $nkey=encode_base64($ref2->encrypt($okey));
		chomp($nkey);
		if (db_exec("UPDATE keys SET secretkey='$nkey' " .
			    "WHERE id=$keys[$i][0]\n") < 0) {
		    db_rollback();
		    fatal("failed to update key id=$keys[$i][0]");
		}
		print "key: $name old: $keys[$i][1] new: $nkey\n"
		  if ($opt_verbose);
	    }
	}
    }

    unless (open(FILE,">$keyfile")) {
	db_rollback();
	fatal("cannot write to file: $keyfile");
    }
    print FILE "Key: ".encode_base64($key)."\n";
    close(FILE);
    fatal("failed to commit key updates to database!")
	if (db_commit() < 0);
    print "Sauron master key set.\n";
}



sub addkey() {
    my($add) = @_;
    my($algo,$mode);

    fatal("Sauron master key not defined/available " .
	  "(generate using --setmasterkey)") unless ($SAURON_KEY);

    my ($server,$name,$type,$len,$comment,$key) = split(/,/,$add);
    $server=getserver($server);
    $name=getname($name);
    $comment=~s/^\s+|\s+$//g;

    fatal("Key with same name allready exists: $name")
	if (get_key_by_name($server,$name) > 0);

    $type=gettype($type);
    if ($type eq  'TSIG') {
	$len=getlen($len,1,512,128);
	$algo=157;
    } else {
	fatal("unsupported key type");
    }
    
    if ($key) {
	fatal("Key must be in Base64 (MIME) format")
	    unless ($key =~ /^[a-zA-Z0-9+\/=]+$/);
	$mode=1;
    } else {
	print "Generating key...\n";
	my $okey = generate_key($name,$type,$len);
	my $ref = Crypt::RC5->new($SAURON_KEY,16);
	$key=encode_base64($ref->encrypt(decode_base64($okey)));
	print "Key: $okey  Encrypted-Key: $key\n" if ($opt_verbose);
	$mode=0;
    }

    chomp($key);
    fatal("failed to add key: $name")
	if (add_key({type=>1,ref=>$server,name=>$name,protocol=>2,mode=>$mode,
		     algorithm=>$algo,keysize=>$len,secretkey=>$key,
		     comment=>$comment}) < 1);
    
    print "Key added successfully.\n";
}

sub listkeys($) {
    my($args) = @_;
    my($i,@q,$typerule);

    my ($server,$type) = split(/,/,$args);
    $server=getserver($server);
    $type=gettype($type) unless ($type eq '' && $args ne '');
    if ($type > 0) {
	$typerule = " AND algorithm=$type ";
    }

    db_query("SELECT id,type,name,keytype,nametype,protocol,algorithm," .
	     " mode,keysize,publickey,secretkey,comment " .
	     "FROM keys " .
	     "WHERE type=1 AND ref=$server $typerule ORDER by name",\@q);
    if (@q < 1) {
	print "No keys found.\n";
	return;
    }
    print "Key name                  Type     Bits Mode   Comments\n";
    print "------------------------- -------- ---- ------ ------------------------------\n";
    for $i (0..$#q) {
	printf "%-25s %-8s %4d %-6s %-30s\n",$q[$i][2],
	                               $KEY_ALGORITHMS{$q[$i][6]},
	                               $q[$i][8],
	                               ($q[$i][7] == 0 ? 'Auto':'Static'),
	                               substr($q[$i][11],0,30);
	                               
    }
}


sub delkey($) {
    my($args) = @_;

    my ($server,$name) = split(/,/,$args);
    $server=getserver($server);
    $name=getname($name);

    my $id = get_key_by_name($server,$name);
    fatal("cannot find key: $name") unless ($id > 0);
    fatal("failed to remove key: $name") if (delete_key($id) < 0);
    print "Key successfully deleted: $name (id=$id)\n";
}


sub regenkeys($) {
  my($args) = @_;
  my($server,$type) = split(/,/,$args);

  $server=getserver($server);
  $type=gettype($type);

  my @keys;
  db_query("SELECT id,name,algorithm,keysize FROM keys " .
	   "WHERE type=1 AND ref=$server AND mode=0",\@keys);
  fatal("No keys found to regenerate") unless (@keys > 0);


  my $i;
  for $i (0..$#keys) {
    my($id,$name,$algo,$len) = @{$keys[$i]};

    print "Generating key: $name ...\n";
    my $okey = generate_key($name,$type,$len);
    my $ref = Crypt::RC5->new($SAURON_KEY,16);
    my $key=encode_base64($ref->encrypt(decode_base64($okey)));
    chomp($key);
    print "Key: $okey  Encrypted-Key: $key\n" if ($opt_verbose);
    
    fatal("Failed to update key: $name")
      if (update_key({id=>$id,secretkey=>$key}) < 0);
  }

}

# eof :-)

