[nym.alias.net] Some extremely brief instructions on setting up a nym.alias.net style nym server. This assumes you already know about remailers and DNS, and sendmail, and have already setup the appropriate DNS records (either A or MX) to receive mail at a "nym" domain name on your machine. Make sure you have perl5.003 or later, installed with the DB_File module. Don't forget to run h2ph when installing perl (cd /usr/include; h2ph *.h sys/*.h). If you don't have DB_File, get db from ftp.cs.berkeley.edu:/ucb/4bsd/db.1.85.tar.gz, install it, and then reinstall perl so that the DB_File module is available. Make sure you also have a type 1 remailer running. By default, the type1 remailer executable is assumed to be at /usr/remail/remailer. If you are using the ghio2 remailer, you will need to add the following lines at the start of the main procedure (also make sure you define PGPPATH as it will be needed for access to the randseed.bin file): int lockfd; umask (007); lockfd = open (DIR "/lock", O_CREAT|O_WRONLY, 0777); flock (lockfd, LOCK_EX); #ifdef PGPPATH strcpy(input,"PGPPATH="); strcat(input,PGPPATH); putenv(input); #endif Create a new userid/groupid under which to run the nym. Call it nymuser. Create a directory for your nym server to reside in, for example /usr/nym. Copy the nymserv perl script into /usr/nym, and make three subdirectories of /usr/nym: pgp, queue, and users. Make sure pgp, queue, and users are writeable by user nymuser. Create a PGP key for your nymserver as follows: setenv PGPPATH /usr/nym/pgp pgp -kg Put the corresponding PGP passphrase in the file pgp/passphrase. Create files /usr/nym/users/postmaster.forward and /usr/nym/users/admin.forward which contain your real E-mail address. Create a file users/remailer-key.reply which contains the PGP public key you just created. Create a ring prototype file. echo 'From: nobody@nym.alias.net' > users/remailer-key.reply echo 'Subject: PGP key for nym.alias.net' >> users/remailer-key.reply echo '' >> users/remailer-key.reply pgp +pubring=pgp/pubring.pgp -fkxa >> users/remailer-key.reply cp pgp/pubring.pgp ring-proto.pgp Edit the first few lines of the nymserv executable to set your machine name and domain name of your nym server. Make sure you also have md5sum (if not, get textutils from prep.ai.mit.edu:/pub/gnu). If you have an A record for your nymserver, set up finger support. Put a line like this in /etc/inetd.conf: finger stream tcp nowait nymuser /usr/nym/nymserv nymserv -fingerd Finally, make a new sendmail.cf file which sends all mail to the nym domain name through the nymserver. What follows is an example ".mc" file suitable for use with sendmail 8.7.5. ------------------------------------------------------------------------------- divert(-1) # Example sendmail configuration for a nymserver include(`../m4/cf.m4') dnl dnl Set your Operating system type below dnl OSTYPE(solaris2)dnl dnl dnl These flags are necessary to encure privacy (and to prevent nym dnl from lines from being rewritten with names of actual users in the dnl password file): dnl define(`confPRIVACY_FLAGS', `novrfy,noexpn,noreceipts,restrictmailq,restrictqrun')dnl define(`confFROM_HEADER', `$g')dnl define(`confLOG_LEVEL', `1')dnl define(`confTO_IDENT', `0s')dnl define(`HReceived', `H?R?Received')dnl dnl dnl Recommended: dnl define(`confMIME_FORMAT_ERRORS', `False')dnl dnl dnl Uncomment the following line if you want procmail used for local dnl mail: dnl dnl FEATURE(local_procmail) PUSHDIVERT(7)dnl ###################################### ### alias mailer specification ### ###################################### dnl dnl Note here that 8888 should be changed to the user ID of the nym dnl user, and 9999 should be changed to the group ID of that user. dnl Mnym, P=/usr/nym/nymserv, F=DFMehluS, L=255, T=X-Unix, U=8888 9999, S=10/30, R=20/40, A=nymserv -d $u POPDIVERT`'dnl MAILER(local)dnl MAILER(smtp)dnl LOCAL_RULE_0 dnl dnl Replace nym.alias.net with the actual name of your nymserver. dnl # Redirect the alias mail to the alias mailer R$+<@nym.alias.net.> $#nym $: $1 ------------------------------------------------------------------------------- And here is the nymserv perl script: #!/usr/local/bin/perl -Tw # $Revision: 1.111 $ require 5.003; use strict; use POSIX qw(:errno_h :fcntl_h); use DB_File; use Socket; require "sys/syscall.ph"; sub LOCK_SH () {0x01;} sub LOCK_EX () {0x02;} sub LOCK_NB () {0x04;} sub LOCK_UN () {0x08;} # Configuration: my $HOMEDIR = '/usr/nym'; my $HOSTNAME = 'nym.alias.net'; my $SENDMAIL = '/usr/lib/sendmail'; my $QMAIL_CODES = 0; # Use qmail rather than sendmail exit codes my $REMAIL = '/usr/remail/remailer'; my $PGP = '/usr/local/bin/pgp'; my $MD5 = '/usr/local/bin/md5sum'; # md5sum from GNU textutils # When things get bad: my $CONFIRM = 0; # Require confirmation of reply-blocks my $NOCREATE = 0; # Don't allow creation of new nyms my $QUOTEREQ = 0; # Quote autoresponder requests my $WARNAFTER = 90; my $DELETEAFTER = 120; # Stuff you probably don't need to change: my $PGPPATH = $HOMEDIR . "/pgp"; my $PASSPHRASEFILE = $PGPPATH . "/passphrase"; my $RINGPROTO = "$HOMEDIR/ring-proto.pgp"; # Pubring with just our pub key my $REPLAY = "$HOMEDIR/replay.db"; # Replay cache my $CCC = "$HOMEDIR/confirm.db"; # Confirmation cookie cache my $NDIR = $HOMEDIR . '/users'; my $QDIR = $HOMEDIR . '/queue'; my $QNAM = "q.$$"; my $QPREF = $QDIR . "/" . $QNAM; my $PGPLOCK = "$QDIR/pgplock"; # Lock for unique access to randseed.bin my $MAXLINES = 1024; my $MSGPERDAY = 512; my $MSGSZUNIT = 32768; my $MSGSIZE = 10240; my $SIGDAYS = 7; # Number of days a digital signature is good for # Flags in user ".dat" file: my $FL_ENCRECV = 0x1; my $FL_SIGSEND = 0x2; my $FL_ACKSEND = 0x100; my $FL_DISABLED = 0x200; my $FL_FIXEDSZ = 0x400; my $FL_FINGERKEY = 0x800; my $FL_NOBCC = 0x1000; my $FL_NOLIMIT = 0x10000; my $DEFFLAGS = 0xfd; my @RSVD_NAMES = ("config", "send", "list", "root", "nobody", "used", "confirm", "postmaster"); undef %ENV; $ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin'; $ENV{'SHELL'} = '/bin/sh'; $ENV{'IFS'} = ' \t\r'; $ENV{'PGPPATH'} = $PGPPATH; $SIG{'TERM'} = \&handler; my $PASSPHRASE; my $msgmd5; # MD5 hash of message signature. my $rmsgid; # Unique message identifier for received messaged my $pgppid; # Process ID of child PGP process my $lockuser; # User whose files are locked my $lockcount = 0; # Number of times locked sub fsync (\*) { my ($f) = @_; select ((select ($f), $|=1)[0]); syscall (&SYS_fsync, fileno ($f)); return 1; } sub sync ($) { my ($path) = @_; local (*F); open (F, "<+$path") || return undef; fsync (*F); close (F); return 1; } sub wipefile (@) { local (*F); my ($file, $nblocks, $i); my $zeroes = pack "x8192"; my $sigsave = $SIG{'TERM'}; $SIG{'TERM'} = 'IGNORE'; foreach $file (@_) { if (open F, "<+$file") { if ($nblocks = (stat F)[7]) { $nblocks = ($nblocks + 0x1fff) >> 13; for ($i = 0; $i < $nblocks; $i++) { print F $zeroes; } } fsync (*F); close F; truncate ("$file", 0); } unlink $file; } $SIG{'TERM'} = $sigsave; } sub saferename ($$) { my ($old, $new) = @_; my $tf = "$QPREF.w"; wipefile ($tf); sync ($old); my $linked = link $new, $tf; my $ret = rename ($old, $new); if ($linked) { if ($ret) { wipefile ($tf); } else { unlink ($tf); } } return $ret; } sub copyfile (**) { my ($in, $out) = @_; my ($buf, $len) = (""); for (;;) { $len = read $in, $buf, 16384; ($len <= 0) && return $len; ((print {$out} $buf) < 0) && return -1; } } sub catfile { local (*F); my $ret = ""; if (open F, "<" . $_[0]) { while () {$ret .= $_;} close (F); } else { die "$_[0]: $!"; } $ret; } my $rndbuf = ""; sub randomval () { if (length ($rndbuf) < 4) { unlink ("$QPREF.rnd"); &runpgp ("+makerandom=20 $QPREF.rnd"); $rndbuf = &catfile ("$QPREF.rnd"); unlink ("$QPREF.rnd"); } my $val = vec ($rndbuf, 0, 32); $rndbuf = substr ($rndbuf, 4); $val; } sub randomstr () { &armor3bytes (&randomval); } sub randomfloat { my $val = &randomval & 0xffff; $val /= 65536.0; $val; } sub flock_db (\%$) { my ($href, $flags) = @_; local (*DB_FH); my $db = tied %$href; $db || fatal (70, "Couldn't get DB object.\n"); $db->sync; open (DB_FH, "+<&" . $db->fd) || die "dup: $!"; flock (DB_FH, $flags) || fatal (70, "Couldn't flock database: $!.\n"); } sub tie_lock (\%$;$) { my ($href, $path, $flags) = @_; my $db; local (*DB_FH); tie (%$href, "DB_File", "$path", O_CREAT|O_RDWR, 0660, $DB_HASH) || fatal (75, "Couldn't open database: $!.\n"); $flags = &LOCK_EX unless defined $flags; flock_db (%$href, $flags) if $flags; } sub untie_unlock (\%) { my ($href) = @_; flock_db (%$href, &LOCK_UN ()); untie %$href; } sub clean { local (*D); my $fn; opendir (D, "$QDIR") || goto release; while (defined ($fn = readdir(D))) { if ($fn =~ /^(\Q$QNAM\E.*)/) { unlink "$QDIR/$1"; } } closedir (D); release: if ($lockcount > 0) { $lockcount = 1; &unlock_user ($lockuser); } } sub leave { my $exitcode = shift; &clean; if ($QMAIL_CODES) { if ($exitcode == 71 || $exitcode == 74 || $exitcode == 75) { $exitcode = 111; } elsif ($exitcode) { $exitcode = 100; } } exit ($exitcode); } sub rollback { if ($msgmd5) { my %rpc; tie_lock (%rpc, $REPLAY); delete $rpc{$msgmd5}; untie_unlock (%rpc); } } sub handler { &rollback; kill 15, $pgppid if $pgppid; &leave (75); } sub fatal { printf @_[1..$#_]; &leave ($_[0]); } sub runpgp ($;$$) { my ($cmd, $passphrase) = @_; my ($ret, $out, $oldflush); local (*LF); pipe (RPP, WPP) || die "pipe failed" if ($passphrase); pipe (ROP, WOP) || die "pipe failed"; if ($PGPLOCK) { open LF, ">$PGPLOCK"; flock LF, &LOCK_EX; } $oldflush = $|; # Flush STDOUT before forking $| = 1; print ""; unless ($pgppid = fork) { close LF; close ROP; #print (STDERR "+ $PGP +batchmode +force +verbose=0 $cmd\n"); open (STDOUT, ">&WOP") || die "couldn't reopen stdout"; open (STDERR, ">&WOP") || die "couldn't reopen stdout"; close (STDIN); close WOP; if ($passphrase) { close WPP; $ENV{'PGPPASSFD'} = fileno RPP; } unless (exec ("$PGP +batchmode +force +verbose=0 +armorlines=0" . " $cmd")) { print STDERR "Exec of PGP failed.\n"; exit (1); } } $| = $oldflush; close WOP; # Be prepared for PGP to hang alarm (120); if ($passphrase) { close RPP; print WPP "$passphrase\n"; close WPP; } $out = ""; while () { $out .= $_; } close (ROP); waitpid ($pgppid, 0); undef $pgppid; alarm (0); $ret = $?; if ($PGPLOCK) { flock LF, &LOCK_UN; close LF; } # printf STDERR "PGP status: 0x%x\n", $ret; $_[2] = $out; return ($ret>>8); } my @BINCHARS = ('-', '0' .. '9', 'A' .. 'Z', '_', 'a' .. 'z'); sub armor3bytes { my ($val) = @_; my $str = ""; my $i; for ($i = 0; $i < 4; $i++) { $str .= $BINCHARS[$val&0x3f]; $val >>= 6; } return $str; } # lineofgarbage generates text that should be hard to compress, not # that is cryptographically random. srand 0; sub lineofgarbage { my ($str, $i) = (""); for ($i = 0; $i < 16; $i++) { $str .= armor3bytes (int (rand 0x1000000)); } return $str; } sub mkmsgid { my $tv = pack ("x16"); syscall (&SYS_gettimeofday, $tv, undef); my ($now, $usec) = unpack ("L2", $tv); my $id = $BINCHARS[($usec>>6) & 0x3f] . $BINCHARS[$usec & 0x3f]; foreach ($now, (($now >> 24) & 0x7f) | ($$ << 8)) { $id .= &armor3bytes ($_); } return $id . &randomstr; } sub rcvdline { my ($user, $plus) = @_; my $msgid = &mkmsgid; return ("Received: by $HOSTNAME with unique id $msgid for " . "<$user" . ($plus ? $plus : "") . "\@$HOSTNAME>; " . scalar (gmtime) . " +0000 (GMT)\n", $msgid); } sub filecost ($) { my $file = shift; my $size = (stat $file)[7]; return 1 unless $size; return 1 + int ($size/$MSGSZUNIT); } my $BGARBAGE = "-----BEGIN GARBAGE-----\n"; my $EGARBAGE = "-----END GARBAGE-----\n"; my $MIMEHDR = <<"EOF"; References: <%s> Subject: Partial message (part %d of %d) X-Garbage: %s Content-type: message/partial; id=\"%s\"; number=%d; total=%d MIME-Version: 1.0 EOF sub remail { my ($file, $sign, $pubring, $rbfile, $fixedsz) = @_; my ($nrbused, $ascfile, $err) = (0); if ($pubring) { $ascfile = "$file.asc"; unlink ("$ascfile"); &runpgp (($sign ? "-seat +pubring=$pubring -u $HOSTNAME" : "-eat +pubring=$pubring +secring=/dev/null") . " $file 0x -o $ascfile", $PASSPHRASE, $err); } else { $ascfile = "$file"; $err = "Fatal internal error.\n"; } &fatal (0, "Encrypt/sign file:\n$err") unless (-f "$ascfile"); open (RB, "<$rbfile") || &fatal (0, "Can't open reply block file.\n"); open (M, "<$ascfile") || &fatal (0, "Missing message file?\n"); my @breakpos = (0); my $padbytes; if ($fixedsz) { my $len = 0; while () { if ($len + length > $MSGSIZE) { fatal (0, "Line too long!\n") if ($len <= 0); push @breakpos, $breakpos[$#breakpos] + $len; $len = 0; } $len += length; } $padbytes = $MSGSIZE - $len; } my $partno = 0; my $nparts = @breakpos; while (@breakpos) { my $line; my %rnd; my %pr; $partno++; seek (RB, 0, 0); for ($line = ; $line && $line =~ /^Reply-Block:\s*(([a-z])=(0.\d+))?/i;) { my ($set, $val) = ($2, $3); resend: my $pos = $breakpos[0]; seek (M, $pos, 0); if ($set) { if (!defined ($rnd{$set})) { $rnd{$set} = &randomfloat; $pr{$set} = 0.0; } if ($pr{$set} < $rnd{$set} && $pr{$set} + $val >= $rnd{$set}) { $nrbused++; open (RM, "| $REMAIL"); } else { open (RM, ">/dev/null"); } $pr{$set} += $val; } else { $nrbused++; open (RM, "| $REMAIL"); } print RM "From nobody\@$HOSTNAME\n\n"; while (defined ($line = ) && $line !~ /^Reply-Block:/i) { print RM $line; } if ($fixedsz && $nparts > 1) { my ($n, $garbage) = (0, ""); $n = $MSGSIZE - ($breakpos[1] - $breakpos[0]) if ($breakpos[1]); $n += 2 if ($partno < 10); $n += 2 if ($nparts < 10); while (length ($garbage) < $n) { $garbage .= substr (&lineofgarbage, 0, $n); } printf RM ($MIMEHDR, $rmsgid, $partno, $nparts, $garbage, $rmsgid, $partno, $nparts); } while () { print RM $_; if ($breakpos[1]) { $pos += length; last if ($pos >= $breakpos[1]); } } if ($fixedsz && $partno == $nparts) { my $n = $padbytes - length ($BGARBAGE . $EGARBAGE); if ($nparts == 1) { $n += length ($MIMEHDR) + 16 - 2; } if ($n > 0) { print RM $BGARBAGE; while ($n > 65) { print RM &lineofgarbage, "\n"; $n -= 65; } print RM substr (&lineofgarbage, 0, $n - 1), "\n" if ($n > 0); print RM $EGARBAGE; } elsif ($padbytes > 0) { print RM "-" x ($padbytes - 1), "\n"; } } close (RM) || fatal (0, "Couldn't write to remailer.\n"); if ($set && $pr{$set} >= 1.0) { $val = $pr{$set} - int ($pr{$set}); $pr{$set} = 0.0; $rnd{$set} = &randomfloat; goto resend if ($val > 0.0); } } shift @breakpos; } close (RB); close (M); return $nrbused * ($fixedsz ? $nparts : filecost ($ascfile)); } sub sendtouser { my ($user, $file, $dontsign, $newrb) = @_; my $flags = (&read_user_dat ($user))[3]; my $crypt = ($flags & $FL_ENCRECV); my $rb = $newrb ? "$NDIR/$user.nrb" : "$NDIR/$user.rb"; return &remail ($file, !$dontsign && $crypt, $crypt ? "$NDIR/$user.pgp" : undef, $rb, $flags & $FL_FIXEDSZ) if $rb; return 0; } sub decrypt_stdin { my $err; open (I, ">$QPREF.i") || &fatal (71, "Error creating queue file ($!).\n"); while () { last if /^-----BEGIN PGP MESSAGE-----\s*$/; } while ($_) { &fatal (71, "Error writing queue file ($!).\n") unless print I $_; last if (/^-----END PGP MESSAGE-----\s*$/ || ! ($_ = )); } &fatal (71, "Error writing queue file ($!).\n") unless close (I); &runpgp ("-b $QPREF.i -o $QPREF.m", $PASSPHRASE, $err); &fatal (66, "Could not decrypt message.\n", $err) unless (-f "$QPREF.m"); } my $TODAY; sub day_number () { $TODAY = int (time / (60 * 60 * 24)) unless defined ($TODAY); return ($TODAY); } sub lock_user { my ($user) = @_; if ($lockcount > 0) { if ($user ne $lockuser) { &fatal (70, "Attempt to lock multiple users.\n"); } else { $lockcount++; return 1; } } $lockuser = $user; do { open LOCKFD, ">$NDIR/$user.lock"; unless (flock (LOCKFD, &LOCK_EX)) { &rollback; &fatal (71, "Can't lock user: $!\n"); } } until ((stat LOCKFD)[3] > 0); $lockcount = 1; return 1; } sub unlock_user { my ($user) = @_; if (!$lockcount || $lockuser ne $user) { &fatal (70, "Attempt to unlock user when not locked.\n"); } if (--$lockcount == 0) { flock (LOCKFD, &LOCK_UN); if (flock (LOCKFD, &LOCK_EX|&LOCK_NB) && (stat LOCKFD)[3] > 0) { unlink "$NDIR/$user.lock"; } close LOCKFD; } } sub read_user_dat { my $user = lc shift; my $today = &day_number; my @vals; &check_username ($user) || return undef; open (DAT, "<$NDIR/$user.dat") || return ($today, 0, $today, $DEFFLAGS); $_ = ; unless (@vals = /^(\d+) (\d+) (\d+) (\d+)/) { warn "Bad data file $NDIR/$user.dat."; # XXX return ($today, 0, $today, $DEFFLAGS); } $_ = ; chop; return (@vals, $_); } sub write_user_dat { my ($user, $upcount, $upsdate, $flags, $name) = @_; my $today = &day_number; my $locked; &lock_user ($user); my @vals = &read_user_dat ($user); unless (@vals) { &unlock_user ($user); return undef; } if ($today != $vals[0]) { $vals[0] = $today; $vals[1] = 0; } if ($upcount > 0) { $vals[1] += $upcount; } if ($upcount < 0) { $vals[1] = 0; } if ($upsdate) { $vals[2] = $today; } if (defined ($flags)) { $vals[3] = $flags; } if (defined ($name)) { $vals[4] = $name; } my $path = &tmpfile ("$vals[0] $vals[1] $vals[2] $vals[3]\n$vals[4]\n"); sync ($path); rename ($path, "$NDIR/$user.dat"); &unlock_user ($user); return $vals[1]; } sub bump_msg_count { my ($user, $inc) = @_; write_user_dat ($user, $inc); my ($count, $flags) = (read_user_dat ($user))[1,3]; my $date = gmtime; return 1 if ($flags & $FL_NOLIMIT); return 1 unless (defined ($count) && $count > $MSGPERDAY); $flags |= $FL_DISABLED; write_user_dat ($user, 0, 0, $flags); &sendtouser ($user, &msgfile ($user, <<"EOF", $count + 1)); From: config\@$HOSTNAME Date: $date GMT To: $user\@$HOSTNAME You have sent sent or received enough mail in one day to exceed the maximum daily limit. Your alias account <$user\@$HOSTNAME> has therefore been disabled. Please check to make sure this isn\'t a configuration error or a loop in your reply block. To reenable your mail alias account, you must send the following commands, PGP signed and encrypted, to : Config: From: $user Nym-Command: -disable EOF return undef; } sub parse_address { $_ = $_[0]; /^.*<(\S*)>/ && return $1; /^(\S+:)?\s*(\S+)/ && return $2; undef; } sub reply_hdr { my $prio = 0; my $sender; my $msgid; my $hdr; my $qhdr = ''; while () { $qhdr .= "> $_"; if (/^$/) { last; } elsif (/^Message-Id:\s*(<\S+>)/) { $msgid = $1; } elsif (/MAILER-DAEMON/) { return (); } elsif (/^From\s+(\S+)/ && $prio < 1) { $prio = 1; $sender = $1; } elsif (/^From:/ && $prio <= 2) { $prio = 2; $sender = &parse_address ($_); } elsif (/^Reply-To:/) { $prio = 3; $sender = &parse_address ($_); } elsif (/^X-Loop: $HOSTNAME$/) { return (); } } return () unless ($sender); $hdr = "To: $sender\n"; $hdr .= "References: $msgid\nIn-Reply-To: $msgid\n" if ($msgid); $hdr .= "X-Loop: $HOSTNAME\n"; return ($hdr, $qhdr); } sub get_user { my ($from) = @_; my ($user, $extra); ($from =~ /^From:\s*(\w[\w-]{1,15})(\+[\w-]*)?(@($HOSTNAME)\b[^@]*| [^@]*|)\s*$/i) || return undef; $user = $1; $extra = $2 ? $2 : ""; &check_username ($user) || return undef; return ($user, $extra); } # Calculate some number that grows roughly as the number of days in a # given date. This way we can make sure a signature was made at most # 6-8 days ago (since we will get leap years wrong). my @dom = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); sub sumtime { my ($y, $m, $d) = @_; my $i; $d += 365 * $y; for ($i = 0; $i < $m; $i++) { $d += $dom[$i]; } return ($d); } sub check_replay { my ($file, $err) = @_; my %rpc; # Replay cache ($err =~ /^Signature made (\d{4})\/(\d{2})\/(\d{2}) /m) || return (-1); my $sigday = &sumtime ($1 - 1900, $2 - 1, $3 + 0); my $today = &sumtime ((gmtime)[5,4,3]); ($today >= $sigday + $SIGDAYS) && return (1); ($sigday > $today + 1) && &fatal (65, "Invalid date on PGP signature\n"); my $sighash = `$MD5 $file`; $sighash =~ s/ .*\n//; tie_lock (%rpc, $REPLAY); if (defined ($rpc{$sighash})) { untie_unlock (%rpc); return 1; } $msgmd5 = $rpc{$sighash}; $rpc{$sighash} = $sigday; $rpc{'.clean'} = 0 unless (defined ($rpc{'.clean'})); if ($today > $rpc{'.clean'}) { foreach (keys (%rpc)) { delete ($rpc{$_}) if ($today > $rpc{$_} + $SIGDAYS); } $rpc{'.clean'} = $today; } untie_unlock (%rpc); return undef; } sub check_sig { my ($pubring, $message) = @_; my $err; if (&runpgp ("+secring=/dev/null +pubring=$pubring" . " $message.sig $message", undef, $err) || ($err =~ /^Good signature from user.*<(config|send)\@$HOSTNAME>/m)) { $_[2] = "Invalid PGP signature.\n"; return undef; } if (&check_replay ("$message.sig", $err)) { &fatal (0, "Discarding replay or old message.\n"); # $_[2] = "Message replay or invalid date on PGP signature.\n"; # return undef; } return 1; } sub check_username { my ($user) = lc shift; if ($user !~ /^\w[\w-]{1,15}$/) { $_[1] = "Invalid alias name.\n"; return undef; } if (-f "$NDIR/$user.forward" || -f "$NDIR/$user.reply") { $_[1] = "Alias $user\@$HOSTNAME is reserved.\n"; return undef; } foreach (@RSVD_NAMES) { $_ = lc $_; if ($user eq $_) { $_[1] = "Alias $user\@$HOSTNAME is reserved.\n"; return undef; } } return (1); } sub authorize_user { my ($user) = @_; my $err; unless (&check_username ($user , $err)) { $_[1] = $err; return undef; } unless (&check_sig ("$NDIR/$user.pgp", "$QPREF.m", $err)) { $_[1] = $err; return undef; } &write_user_dat ($user, 0, 1); return 1; } my $tcnt = 0; sub tmpfile { local *TMP; my $path = "$QPREF.t" . $tcnt++; unlink ("$path"); open (TMP, ">$path") || &fatal (70, "Could not create temporary file.\n"); unless ((printf TMP @_) && (close (TMP))) { &fatal (74, "Could not write temporary file.\n"); } return ($path); } sub msgfile { my ($user, @msg) = @_; my ($recvd, $msgid) = &rcvdline ($user); return &tmpfile ("%s$msg[0]", $recvd, (@msg[1..$#msg])); } sub ccc_delete (\%$) { my ($href, $user) = @_; my $cookie; if ($cookie = $$href{$user}) { $cookie =~ s/^\S+\s+//; delete ($$href{$cookie}); delete ($$href{$user}); return 1; } return undef; } sub ccc_cookie (\%$) { my ($href, $user) = @_; my $cookie; if ($cookie = $$href{$user}) { $cookie =~ s/^\S+\s+//; return $cookie; } return undef; } sub ccc_new (\%$) { my ($href, $user) = @_; my $cookie = sprintf "+%08x%08x", randomval, randomval; my $today = &day_number; ccc_delete (%$href, $user); $$href{$user} = "$today $cookie"; $$href{$cookie} = "$today $user"; return $cookie; } sub ccc_clean () { my ($key, $val, $day, $u); my $date = gmtime; my $today = day_number; my %ccc; my @goners; my @killednrb; tie_lock (%ccc, $CCC); foreach $key (keys %ccc) { ($day, $val) = split /\s+/, $ccc{$key}; next unless defined ($day) && defined ($val); next unless $today > $day + $SIGDAYS; delete $ccc{$key}; if ($key !~ /\+/) { if (-f "$NDIR/$key.rb") { wipefile ("$NDIR/$key.nrb"); push @killednrb, $key; } else { push @goners, $key; } } } $ccc{'.clean'} = $today; untie_unlock (%ccc); my $user; foreach $user (@goners) { lock_user ($user); wipefile ("$NDIR/$user.dat", "$NDIR/$user.nrb", "$NDIR/$user.pgp") unless -f "$NDIR/$user.rb"; unlock_user ($user); } foreach $user (@killednrb) { &sendtouser ($user, &msgfile ($user, <<"EOF")); From: confirm\@$HOSTNAME Date: $date GMT To: $user\@$HOSTNAME You submitted a new reply block for your pseudonym a while ago. However, you never confirmed that reply block. This is most likely because the new reply block did not work properly and you never received confirmation instructions. However, it may also be because a message got lost in transit. In either event, if you still want to update your reply block you should send a new copy to config\@$HOSTNAME. EOF } } sub runconfig { my ($user, $pubkey, $rblock, $create, $delete); my $err = ""; my $cmds = ""; &decrypt_stdin; open (M, "<$QPREF.m") || &fatal (66, "Could not decrypt message.\n"); while () { last if (/^Config:/); /^$/ || &fatal (0, "Discarding non-config message.\n"); } $_ || &fatal (0, "Discarding empty input message.\n"); hdrloop: while () { if ($. > $MAXLINES) { $err .= "Header exceeds maximum number of lines.\n"; last; } elsif (/^From:/) { if (defined ($user)) { $err .= "Duplicate From: line\n"; } else { $err .= "Message contains CR characters. " . "Did you forget the -t flag to PGP?\n" if /\r\n/; my $extra; ($user, $extra) = &get_user ($_); $user || ($err .= "Illegal/reserved user name or invalid " . "machine name in From: line.\n"); $extra && ($err .= "Can't send config requests from " . "\"plussed\" addresses\n"); $user =~ tr/A-Z/a-z/ if ($user); } } elsif (/^Public-Key:/i) { if (defined ($pubkey)) { $err .= "duplicate Public-Key: line\n"; } else { $pubkey = ""; while () { $pubkey .= $_; last if /^-----END/; if ($. > $MAXLINES) { undef ($pubkey); redo hdrloop; } } } } elsif (/^Reply-Block:/i) { my $blank = 1; $rblock = $_; while () { if ($. > $MAXLINES) { undef ($rblock); redo hdrloop; } s/\r$//; if (/^Reply-Block:/i && !$blank) { $rblock .= "\n"; } $rblock .= $_; $blank = /^$/; } $rblock .= "\n" unless ($blank); } elsif (/^Nym-Commands?:(.*)/i) { $cmds .= "$1\n"; } elsif (/^([\w-]+:)/) { $err .= "Unknown header \"$1\".\n"; } } close (M); my ($rbfile, $pubring, $pgperr, $flags); if ($rblock) { $rbfile = "$QPREF.rb"; open (RB, ">$rbfile") || &fatal (70, "Can't open reply block file ($!)\n"); &fatal (74, "Error updating reply block ($!).\n") unless ((print RB $rblock) && close (RB)); } if ($pubkey) { $pubring = "$QPREF.pgp"; system ("cp $RINGPROTO $pubring"); open (ASC, ">$QPREF.asc") || &fatal (70, "Can't open public key file ($!)\n"); select ((select (ASC), $| = 1)[$[]); &fatal (74, "Error updating public key file ($!).\n") unless ((print ASC $pubkey) && close (ASC)); if (&runpgp ("-ka +secring=/dev/null" . " +pubring=$pubring" . " $QPREF.asc", undef, $pgperr) || $pgperr !~ /^\s*1 new key/m) { $err .= "Error setting new public key:$pgperr\n"; undef ($pubring); } else { chmod (0660, "$QPREF.pgp"); } } my $incctr = 0; my $fullname; $user && &lock_user ($user); if ($user && $cmds) { my $rest = $cmds; $flags = (&read_user_dat ($user))[3]; while ($rest =~ /^\s*([\w+-]+[=?]?)/) { $_ = $1; $rest = $'; if (/^(\+|-)(\w+)$/) { my $val; if ($2 eq "acksend") { $val = $FL_ACKSEND; } elsif ($2 eq "cryptrecv") { $val = $FL_ENCRECV; } elsif ($2 eq "signsend") { $val = $FL_SIGSEND; } elsif ($2 eq "disable") { $val = $FL_DISABLED; #$incctr = -1 if ($1 eq "-"); } elsif ($2 eq "fixedsize") { $val = $FL_FIXEDSZ; } elsif ($2 eq "fingerkey") { $val = $FL_FINGERKEY; } elsif ($2 eq "nobcc") { $val = $FL_NOBCC; } else { $err .= "Invalid Nym-Command switch `$1' in `$1$2'.\n"; next; } if ($1 eq "+") { $flags |= $val; } else { $flags &= ~$val; } } elsif ($_ eq "name=") { unless ($rest =~ /^\"(([^\\\"$;\n]|\\\\|\\\")*)\"\s/) { $err .= "Could not parse your name=\"...\" Nym-Command.\n"; $rest = ""; last; } $rest = $'; $fullname = $1; $fullname =~ s/\\\"/\"/g; $fullname =~ s/\\\\/\\/g; $err .= "Full names cannot contain unprintable characters.\n" if $fullname =~ /[\x00-\x1f\x7f-\x9f\xff]/; my $s = $fullname; $s =~ s/\\./../g; $s =~ s/\"[^\"]*\"//g; $err .= <<"EOF" if $s =~ /[@\",<>]/; Full names cannot have unbalanced '\"' characters or unquoted '<', '>', '\@', or ',' characters in name= Nym-Commands. EOF $err .= "Full name is too long.\n" if length ($fullname) > 255; } elsif ($_ eq "delete") { $delete = 1; } elsif ($_ eq "create") { $create = 1; } elsif ($_ eq "create?") { $create = 1 unless -f "$NDIR/$user.pgp"; } else { $err .= "Invalid Nym-Command `$_'.\n"; $rest =~ s/^\"(([^\\\"$;\n]|\\\\|\\\")*)\"\s//; } } if ($rest =~ /^\s*(\S[^\n\r]*)/) { $err .= "Could not parse Nym-Command `$1'.\n"; } } $err .= "$HOSTNAME is not currently granting new aliases.\n" if ($create && $NOCREATE); my $authorized; if ($user) { if (! $create) { if (-f "$NDIR/$user.pgp") { ($authorized = &authorize_user ($user, $pgperr)) || ($err .= "$pgperr "); } else { $err = "No such user. Use \"Nym-Command: create\" to " . "create a new alias.\n"; } } else { $err .= "Can't use delete Nym-Command with create.\n" if ($delete); $rbfile || ($err .= "You must specify at least one reply-block" . " with \"Reply-Block\".\n"); if ($pubring) { &check_sig ($pubring, "$QPREF.m", $pgperr) || ($err .= "$pgperr (when checking against the public" . "key in your message)\n"); } else { $err .= "You must specify a PGP public key with" . " \"Public-Key:\".\n"; } if ($err || -f "$NDIR/$user.pgp") { $err .= "The username you chose is already in use.\n" unless ($err); } } } else { $err .= "No/bad From: line designating Nym.\n"; } my $date = gmtime; if ($err) { $user && &unlock_user ($user); my $file = &msgfile ($user ? $user : "UNKNOWN ALIAS", <<"EOF", From: config\@$HOSTNAME Date: $date GMT To: %s%s Your request to modify or create %s could not be performed. The following error(s) were encountered: %s EOF $user ? "$user\@$HOSTNAME" : "UNKNOWN ALIAS", $authorized ? "" : " (unauthentic)", $user ? "alias <$user\@$HOSTNAME>" : "an unspecified alias", $err); if ($authorized && !$rbfile) { &sendtouser ($user, $file); } elsif ($rbfile) { &remail ($file, $authorized, $authorized ? "$NDIR/$user.pgp" : $pubring, $rbfile); } &fatal (0, $err); } my %ccc; if ($delete) { my $usenrb = ""; &fatal (0, "Can't delete when not authorized!\n") unless ($authorized); $SIG{'TERM'} = 'IGNORE'; unlink ("$QPREF.pgp"); if ($CONFIRM) { tie_lock (%ccc, $CCC); ccc_delete (%ccc, $user); untie_unlock (%ccc); $usenrb = "n" if (-f "$NDIR/$user.nrb"); } unless (-f "$QPREF.rb") { rename (("$NDIR/$user." . $usenrb . "rb"), "$QPREF.rb") || &fatal (0, "rename .rb file failed\n"); } wipefile ("$NDIR/$user.rb", "$NDIR/$user.nrb", "$NDIR/$user.dat"); rename ("$NDIR/$user.pgp", "$QPREF.pgp") || &fatal (0, "rename .pgp file failed\n"); &unlock_user ($user); &remail (&msgfile ($user, <<"EOF"), 1, "$QPREF.pgp", "$QPREF.rb"); From: config\@$HOSTNAME Date: $date GMT To: $user\@$HOSTNAME This message is to confirm that pseudonym <$user\@$HOSTNAME> was deleted on $date GMT. Your reply-block and PGP key will be wiped once this message is mailed. EOF wipefile ("$QPREF.rb", "$QPREF.pgp"); &leave (0); } my ($cmsg, $cookie) = (""); tie_lock (%ccc, $CCC) if ($CONFIRM); $SIG{'TERM'} = 'IGNORE'; if ($pubring) { saferename ("$pubring", "$NDIR/$user.pgp"); } if ($rbfile) { if ($CONFIRM) { ccc_new (%ccc, $user); saferename ("$rbfile", "$NDIR/$user.nrb"); } else { saferename ("$rbfile", "$NDIR/$user.rb"); } } &write_user_dat ($user, $incctr, 1, $flags, $fullname); &unlock_user ($user); my $replyto = ''; if ($CONFIRM && ($cookie = ccc_cookie (%ccc, $user))) { $replyto = "Reply-To: confirm$cookie\@$HOSTNAME\n"; $cmsg = <<"EOF"; A new reply block has been received for your mail alias, but has not yet been activated. In order to start receiving mail with your new reply block, you must confirm it by sending an (anonymous) E-mail message to the following address: confirm$cookie\@$HOSTNAME The contents of the message can be anything. Any message delivered to this address will activate your reply block. EOF } my $doclean = ($CONFIRM && !($ccc{'.clean'} && $ccc{'.clean'} == &day_number)); untie_unlock (%ccc) if ($CONFIRM); &sendtouser ($user, &msgfile ($user, $replyto . <<"EOF" . $cmsg), undef, From: config\@$HOSTNAME Date: $date GMT To: $user\@$HOSTNAME Your configuration request completed successfully. EOF defined ($cookie)); $SIG{'TERM'} = \&handler; # Here would be a good place to receive signals. $SIG{'TERM'} = 'IGNORE'; ccc_clean if ($doclean); &leave (0); } sub runconfirm { my $arg = lc shift; my ($user, $cookie); my $date = gmtime; my %ccc; (-f $CCC) || fatal (67, "Invalid username\n"); tie_lock (%ccc, $CCC, 0); ($user = $ccc{$arg}) || &leave (0); $user =~ s/^\S+\s+//; lock_user ($user); flock_db (%ccc, &LOCK_EX ()); delete ($ccc{"c:$arg"}); (($cookie = ccc_cookie (%ccc, $user)) && $cookie eq $arg) || &leave (0); $SIG{'TERM'} = 'IGNORE'; ccc_delete (%ccc, $user); (-f "$NDIR/$user.nrb") || &fatal (70, "No reply block for valid confirmation\n"); saferename ("$NDIR/$user.nrb", "$NDIR/$user.rb") || &fatal (70, "Cannot install new reply block\n"); flock_db (%ccc, &LOCK_UN ()); unlock_user ($user); &sendtouser ($user, &msgfile ($user, <<"EOF")); From: confirm\@$HOSTNAME Date: $date GMT To: $user\@$HOSTNAME Your new reply block has been confirmed and installed. Your mail alias is currently active. EOF &leave (0); } sub runsend { my $user; my $date = gmtime; my $inhdr; my $flags; my $hiddento = ''; my $headerto = ''; my $resentto = ''; my $nymcommands = ''; my $warnings = ''; my $hasbody; my $notmailed = ''; &decrypt_stdin; open (H, ">$QPREF.h"); open (B, ">$QPREF.b"); open (M, "<$QPREF.m") || &fatal (66, "Could not decrypt message.\n"); while () { unless ($inhdr) { /^$/ && next; /^Config:/i && &fatal (0, "Discarding config message to send.\n"); $inhdr = 1; /^::$/ && next; } moreheaders: if ($. > $MAXLINES) { &fatal (70, "Header exceeds maximum number of lines.\n"); } elsif (/^From:/) { my $line = $_; my ($name, $extra); &fatal (0, "duplicate From: line\n") if defined ($user); ($user, $extra) = &get_user ($_); $user || &fatal (0, "Illegal user name or invalid " . "machine name in From: line.\n"); ($flags, $name) = (&read_user_dat ($user))[3, 4]; if ($name) { $line = "From: $name <$user$extra\@$HOSTNAME>\n"; } else { $line = "From: $user$extra\@$HOSTNAME\n"; } (print H $line) || &fatal (71, "Error writing queue file ($!).\n"); $user =~ tr/A-Z/a-z/; next; } elsif (/^Hidden-To:(.*)$/i) { $hiddento .= " $1"; while () { if (/^[ \t]/) {chomp ($hiddento .= $_);} else {goto moreheaders;} } } elsif (/^(Resent-)?(To|Cc|Bcc):(.*)$/i) { my $rl = $1 ? \$resentto : \$headerto; $$rl .= " $3"; (print H $_) || &fatal (71, "Error writing queue file ($!).\n"); while () { if (/^[ \t]/) {chomp ($$rl .= $_);} else {goto moreheaders;} (print H $_) || &fatal (71, "Error writing queue file ($!).\n"); } } elsif (/^Nym-Commands?:(.*)$/i) { $nymcommands .= " $1"; while () { if (/^[ \t]/) {chomp ($nymcommands .= $_);} else {goto moreheaders;} } } if (/^$/) { undef $_; last; } elsif (/^\S/ && !/^\S+:/) { print B $_; $hasbody = 1; undef $_; last; } (print H $_) || &fatal (71, "Error writing queue file ($!).\n"); } print H "\n" unless defined $_; &fatal (65, "No From: line.\n") unless $user; foreach (split ' ', $nymcommands) { next unless $_; /^\+acksend/ && ($flags |= $FL_ACKSEND, next); /^\-acksend/ && ($flags &= ~$FL_ACKSEND, next); /^\+signsend/ && ($flags |= $FL_SIGSEND, next); /^\-signsend/ && ($flags &= ~$FL_SIGSEND, next); $warnings .= "Ignored unknown nym-command `$_' (non-fatal).\n"; } close (H) || &fatal (71, "Error writing queue file ($!).\n"); while () { $hasbody = 1 if !$hasbody && /./; print B $_ || &fatal (71, "Error writing queue file ($!).\n"); if ($hasbody) { copyfile (*M, *B); last; } } close (M); print B <<"EOF" if ($flags & $FL_SIGSEND && $hasbody); ~~~ This PGP signature only certifies the sender and date of the message. It implies no approval from the administrators of $HOSTNAME. Date: $date GMT From: $user\@$HOSTNAME EOF close (B) || &fatal (71, "Error writing queue file ($!).\n"); &authorize_user ($user) || &fatal (0, "Invalid PGP signature on message\n"); if ($CONFIRM && ! -f "$NDIR/$user.rb" && -f "$NDIR/$user.nrb") { my %ccc; my $cookie; lock_user ($user); tie_lock (%ccc, $CCC, &LOCK_SH ()); $cookie = ccc_cookie (%ccc, $user); untie_unlock (%ccc); if (! -f "$NDIR/$user.rb" && -f "$NDIR/$user.nrb" && $cookie) { &sendtouser ($user, &msgfile ($user, <<"EOF"), undef, 1); Reply-To: confirm$cookie\@$HOSTNAME From: send\@$HOSTNAME Date: $date GMT To: $user\@$HOSTNAME You have attempted to send a message through send\@$HOSTNAME. However, the message could not be resent because your alias does not yet have an active reply block. To activate your reply block and make your alias functional, you must send E-mail (anonymously) to this address: confirm$cookie\@$HOSTNAME The contents of the mail you send is not important. Any message delivered to this address will activate your reply block. EOF &fatal (0, "Reply block not activated.\n"); } unlock_user ($user); } if ($flags & $FL_DISABLED) { $notmailed = 'NOT '; $warnings .= "Your nym account is disabled.\n" . "Send -disable Nym-Command to reenable.\n"; } my $result; my @recips; my $err; if (!$notmailed && ($flags & $FL_SIGSEND) && $hasbody) { &fatal (78, "Could not create PGP signature.\n$err") if (&runpgp ("-sat $QPREF.b", $PASSPHRASE, $err) || ! -f "$QPREF.b.asc"); } else { rename "$QPREF.b", "$QPREF.b.asc"; } open (H, "<$QPREF.h"); open (B, "<$QPREF.b.asc"); if ($hiddento) { @recips = grep {$_} split /[\s,]+/, $hiddento;; unless (@recips) { $notmailed = 'NOT '; $warnings .= "No valid recipients in Hidden-To: header.\n"; } } if ($notmailed) { open (SEND, ">/dev/null"); } elsif ($hiddento) { open (SEND, "|-") || exec ($SENDMAIL, "-f", "$user\@$HOSTNAME", "-os", "-oem", "-oi", "--", (@recips)) || &fatal (1, "Couldn't run $SENDMAIL @recips\n"); } else { open (SEND, "| $SENDMAIL -f $user\@$HOSTNAME -os -oem -oi -t"); } copyfile (*H, *SEND); copyfile (*B, *SEND); $result = close (SEND); unless ($notmailed) { my ($nrecips, $to) = (1); ($to = $hiddento) || ($to = $resentto) || ($to = $headerto); $to =~ s/\([^\)]*\)//; if ($to) { my @rl = split /[\s,]+/, $to; $nrecips = @rl; } bump_msg_count ($user, $nrecips, filecost ("$QPREF.b.asc")); } undef $msgmd5; if ($result && ($flags & $FL_ACKSEND) || $hiddento && !@recips || $warnings || $notmailed) { seek H, 0, 0; open ACK, ">$QPREF.a"; print ACK (&rcvdline ($user))[0]; print ACK <<"EOF"; From: send\@$HOSTNAME Date: $date GMT To: $user\@$HOSTNAME EOF if ($hiddento) { if (@recips) { print ACK <<"EOF", "\t", join (",\n\t", @recips), "\n"; A message with the following header was ${notmailed}remailed to these recipients under your pseudonym: EOF } else { print ACK <<"EOF"; The message you sent with the following header could not be remailed because no valid recipients were found in the 'Hidden-To:' header. EOF } } else { print ACK <<"EOF"; This is to acknowledge that a message with the following header was ${notmailed}remailed under your pseudonym: EOF } print ACK "\n"; while () { print ACK $_; } print ACK "\nSome problems were encountered with your " . "message:\n\n", $warnings if ($warnings); close (ACK); &sendtouser ($user, "$QPREF.a"); } close (H); close (B); &leave (0); } sub runreceive { my ($user, $extra) = @_; $extra = "" unless (defined ($extra)); my $date = gmtime; my $flags = (&read_user_dat ($user))[3]; my ($recip, $rrecip) = ('', ''); if ($flags & $FL_DISABLED) { write_user_dat ($user, 1); &fatal (69, "Account disabled.\n"); } open (I, ">$QPREF.i") || &fatal (71, "Error creating queue file ($!).\n"); my $rcvd; $extra = '' unless defined $extra; ($rcvd, $rmsgid) = &rcvdline ($user, $extra); print I $rcvd; while () { restart: s/^From /X-From: /; &fatal (71, "Error writing queue file ($!).\n") unless print I $_; last if /^(\s*|[^\s:]+(\s.*)?)$/; if (/^(Resent-)?(To|Cc):(.*)$/i) { my $rp = $1 ? \$rrecip : \$recip; $$rp .= $3; while () { goto restart unless /^\s/; &fatal (71, "Error writing queue file ($!).\n") unless print I $_; $$rp .= $_; } } } if (!$extra && ($flags & $FL_NOBCC)) { my $erecip = "$user$extra\@$HOSTNAME"; $recip = $rrecip if $rrecip; unless ($recip =~ /\b\Q$erecip\E\b/i) { &fatal (77, <<"EOF"); This user does not wish to receive blind carbon copies. You must specify the user\'s full E-mail address in a To or Cc mail header. EOF } } copyfile (*STDIN, *I); print I <<"EOF" if ($flags & $FL_ENCRECV); ~~~ This PGP signature only certifies the receipt and date of the message. It implies no approval from the administrators of $HOSTNAME. Date: $date GMT To: $user\@$HOSTNAME EOF close (I) || &fatal (71, "Error writing queue file ($!).\n"); my $nmsg = &sendtouser ($user, "$QPREF.i", !($flags & $FL_ENCRECV)); bump_msg_count ($user, $nmsg); &leave (0); } sub usage { my $prog = $0; $prog =~ s/.*\///; fatal (64, <<"EOF"); Usage: $prog -d user $prog -fingerd $prog -wipe user ... $prog -expire [-f] EOF } sub usednyms () { opendir (D, $NDIR) || return (); my $last = ''; my @list = grep ({($last ne $_, ($last = $_))[0]} (sort @RSVD_NAMES, grep (s/^(.*)\.(pgp|forward|reply)$/$1/, readdir D))); closedir (D); return @list; } sub rundeliver { chop ($PASSPHRASE = catfile ($PASSPHRASEFILE)); &clean; fatal (64, "Usage: nymserv -d recipient\n") unless (@ARGV == 1); fatal (67, "Invalid username\n") unless ($ARGV[0] =~ /^(\w[\w-]{1,15})(\+[\w-]*)?$/); my $recip = lc $1; my $plussed = $2; if ($recip eq "nobody") { &leave (0); } elsif ($recip eq "config") { &runconfig; } elsif ($recip eq "send") { &runsend; } elsif ($recip eq "confirm" && $plussed) { &runconfirm ($plussed); } elsif ($recip =~ /^(list|used)$/) { my ($H, $Q) = &reply_hdr; &leave (65, "No valid return address found\n") unless $H; my @list = &usednyms; open SM, "| $SENDMAIL -oi -f nobody\@$HOSTNAME -t"; print SM $H; print SM "Subject: list of used nyms on $HOSTNAME\n"; print SM "From: used nyms \n\n"; print SM join ("\n", @list), "\n"; print SM "\n", $Q if ($QUOTEREQ); &leave (0); } elsif (-r "$NDIR/$recip.forward") { my $inb; my $to; chop ($to = `cat $NDIR/$recip.forward`); ($to =~ /^(.*)$/) && ($to = $1); if ($to =~ /^\|(.*)/) { exec $1 || &fatal (72, "Exec of $1 failed\n"); } open SM, "| $SENDMAIL -oi -f nobody\@$HOSTNAME -- $to"; while () { unless ($inb) { s/^From /X-From: /; s/^(Return-Receipt-To:)/X-$1/i; s/^(Notice-Requested-Upon-Delivery-To:)/X-$1/i; s/^(Errors-To:)/X-$1/i; $inb = 1 if (/^$/); } print SM $_; } close (SM); &leave (0); } elsif (-f "$NDIR/$recip.reply") { my ($H, $Q) = &reply_hdr; &leave (65, "No valid return address found\n") unless $H; open SM, "| $SENDMAIL -oi -f nobody\@$HOSTNAME -t"; print SM $H; open F, "<$NDIR/$recip.reply"; while () { print SM $_; } print SM "\n", $Q if ($QUOTEREQ); close (F); close (SM); &leave (0); } elsif (-r "$NDIR/$recip.pgp") { (-r "$NDIR/$recip.rb") || &fatal (69, "Account disabled.\n"); &runreceive ($recip, $plussed); } else { fatal (67, "Invalid username\n"); } &leave (0); } sub runfingerd { my ($target, $warning, $fingerdir); my $BLURB = ucfirst <<"EOF"; $HOSTNAME offers untraceable E-mail pseudonyms. Finger help\@$HOSTNAME for more information about this service, or visit http://www.cs.berkeley.edu/~raph/n.a.n.html. EOF $SIG{'ALRM'} = sub {&fatal (1, "fingerd timed out\n");}; alarm (30); $target = ; if ($target) { $target =~ s/\r?\n//; } else { $target = ''; } if ($target =~ /(.*)@([^@]+)/) { my ($user,$host,$port,$ipaddr,$sin,$myaddr) = ($1, $2); $host .= "." unless $host =~ /\d{1,3}(\.\d{1,3}){1,3}/; &fatal (1, "unknown host: %s\n", $host) unless ($ipaddr = inet_aton ($host)); &fatal (1, "internal error: unknown service finger\n") unless ($port = getservbyname ('finger', 'tcp')); socket (SOCK, PF_INET, SOCK_STREAM, getprotobyname ('tcp')) || &fatal (1, "internal error: could not create socket\n"); $sin = sockaddr_in ($port, $ipaddr); printf "[finger %s@%s]\n", $user, $host; $myaddr = (unpack_sockaddr_in (getsockname (STDIN)))[1]; bind (SOCK, sockaddr_in (0, $myaddr)) || &fatal (1, "internal error: can't bind socket\n"); &fatal (1, "Connection refused while connecting to %s\n", $host) if ($ipaddr eq INADDR_LOOPBACK); connect (SOCK, $sin) || &fatal (1, "$! while connecting to %s\n", $host); select ((select (SOCK), $| = 1)[$[]); printf SOCK "%s\r\n", $user; while () { print; } exit (0); } $target =~ tr/A-Z/a-z/; $target =~ s/^\/w//; # RFC 742 is weird if ($target =~ /^([\w][\w-]{0,15})$/) { $target = $1; } else { if ($target) { print "finger: $target: no such user.\n"; } else { print $BLURB; print <<"EOF"; In addition to fingering individual E-mail aliases, you can finger: remailer-key - Public key for the $HOSTNAME nym server. help - Help file for the $HOSTNAME nym server. list - List of taken pseudonyms. EOF } exit; } $target = 'remailer-key' if ($target eq 'config' || $target eq 'send'); if ($target =~ /^(list|used)$/) { unless (chdir ($NDIR)) { print "No information on $target.\n"; exit; } print "List of pseudonyms in use on $HOSTNAME:\n\n"; print join("\n", &usednyms), "\n"; } elsif (-r "$NDIR/$target.reply") { unless (open (DATA, "<$NDIR/$target.reply")) { print "No information on $target.\n"; exit; } while () { last unless /^\S+:/; } print if $_ && !/^$/; print while (); } elsif (-r "$NDIR/$target.forward") { printf STDOUT ("Mail Alias: %-24s Name: %s\n", $target, '???'); print "\n", $BLURB; } elsif (-r "$NDIR/$target.dat") { my ($flags, $fullname) = (&read_user_dat ($target))[3,4]; printf STDOUT ("Mail Alias: %-24s Name: %s\n", $target, ($fullname && $fullname =~ /\S/) ? $fullname : "???"); if ($flags & $FL_FINGERKEY) { my $key; &runpgp ("-fkxa '<$target\@$HOSTNAME>' $NDIR/$target.pgp" . " 2> /dev/null", undef, $key); print "PGP Public-Key:\n$key" if $key =~ /^-----BEGIN PGP PUBLIC KEY BLOCK-----/; } print "\n", $BLURB; } else { print "finger: $target: no such user.\n"; } &leave (0); } sub runwipe { my $user; my %ccc; tie_lock (%ccc, $CCC, 0); foreach $user (@_) { unless ($user =~ /^(\w[\w-]*)$/) { warn "bad user: $user"; next; } $user = $1; lock_user ($user); flock_db (%ccc, &LOCK_EX ()); ccc_delete (%ccc, $user); wipefile ("$NDIR/$user.rb", "$NDIR/$user.nrb", "$NDIR/$user.dat", "$NDIR/$user.pgp"); flock_db (%ccc, &LOCK_UN ()); unlock_user ($user); } untie_unlock (%ccc); } sub keyinfo ($;$) { my ($ring, $user) = @_; my ($result, $bits, $id, $fingerprint); $user = "0x" unless defined $user; &runpgp ("-kvc $user $ring", undef, $result); $result || return (); ($bits, $id) = ($result =~ /^pub\s+(\d+)\/(\w+)/m); ($fingerprint) = ($result =~ /^\s+Key fingerprint\s*=\s*(\S.*)$/m); return ($bits, $id, $fingerprint); } sub runexpire { my $force = shift; $force = undef unless (defined ($force) && $force eq '-f'); my ($nym, $days); my @warn; my @expire; &day_number; opendir N, "$NDIR"; foreach $nym (grep {/\.dat$/} readdir (N)) { $nym =~ s/\.dat$//; $days = $TODAY - (&read_user_dat ($nym))[2]; if ($days > $DELETEAFTER) { push @expire, $nym; } elsif ($days > $WARNAFTER) { push @warn, $nym; } } closedir N; if ($force) { &runwipe (@expire); foreach $nym (@warn) { my $keyid = (keyinfo ("$NDIR/$nym.pgp"))[1]; $keyid = $keyid ? "0x$keyid" : 'yournym_PGP_key_ID'; open SM, "|$SENDMAIL -f nobody\@$HOSTNAME -t"; print SM <<"EOF"; From: Alias expiration daemon To: $nym\@$HOSTNAME Subject: Your mail alias is expiring In order to clean up abandoned mail aliases or aliases with lost PGP keys, a daemon periodically deletes pseudonyms which appear to be unused. It has been over $WARNAFTER days since the last time $HOSTNAME received a piece of E-mail signed by your private key. Your pseudonym is therefore considered inactive, and will be deleted if it is still inactive after $DELETEAFTER days. If you do not wish to have your mail alias deleted, simply send any piece of E-mail through , or any configuration message to --your pseudonym will then automatically be renewed. This may also be a good time update your reply block with a new list of remailers and Encrypt-Keys, as the list of reliable remailers can change from month to month and any configuration message will renew your pseudonym. Note that receiving mail through your pseudonym does not renew it, as an abandoned account could still be receiving mail. In order to renew your account, you must show the nym server that someone still has access to your account\'s private PGP key. Note further that just changing the mail header of your outgoing mail to show your $HOSTNAME address will not renew your pseudonym either. (This is insecure anyway and so is not recommended in the first place.) The simplest way to renew your pseudonym is to create a file called, for instance, renew, with the following contents (indented one space here for clarity, do not indent it when you create the file): Config: From: $nym Then sign and encrypt this message with this pgp command: pgp -seat renew config\@$HOSTNAME -u $keyid Here $keyid corresponds to the hex key ID of the PGP key under which you created your pseudonym account. (You can also use the descriptive name of your PGP key instead of the hex key ID. If you are unsure what you named your PGP key, you can check the entire contents of your keyring with the command pgp -kv.) The above pgp command will create a file called renew.asc. You must then mail the contents of that file to , either directly, or, preferably, through some anonymous remailers. When the nym server receives your message, it will send you a confirmation saying 'Your configuration request completed successfully.' Once you receive this confirmation, your alias account will have been renewed. Note: You must create a new renew.asc file each time you renew your pseudonym. Nym.alias.net has a replay cache which prevents an intercepted message from being replayed multiple times by an attacker. Thus, each signed renew.asc file will only work once, and will only work if the date in the PGP signature is current. If you have any questions about this policy or other problems with $HOSTNAME, please raise them on the newsgroup alt.privacy.anon-server, or contact admin\@$HOSTNAME as a last resort. EOF close SM; sleep 2; } } if (@warn || @expire) { print "Pseudonym expiration report:\n"; print "warned: @warn\n\n" if (@warn); print "expired: @expire\n" if (@expire); } } umask (007); $( = (split /\s+/, $), 2)[0]; $< = $>; chdir ($HOMEDIR) || die "$HOMEDIR: $!"; $SIG{'ALRM'} = \&handler; my $flag = shift; if (!$flag) { &usage; } elsif ($flag eq '-d') { &rundeliver (@ARGV); } elsif ($flag eq '-fingerd') { &runfingerd; } elsif ($flag eq '-wipe') { &runwipe (@ARGV); } elsif ($flag eq '-expire') { &runexpire (@ARGV); } else { &usage; } &leave (0);