/* -*-C-*-
*******************************************************************************
*
* File:         PGP.m
* RCS:          /usr/local/sources/CVS/EnhanceMail/PGP.m,v 1.18 1998/07/12 10:51:10 tom Exp
* Description:  
* Author:       Carl Edman
* Created:      Fri Oct 13 11:48:05 1995
* Modified:     Sun Jun  7 20:24:34 1998 Tom Hageman
* Language:     C
* Package:      N/A
* Status:       Experimental (Do Not Distribute)
*
* (C) Copyright 1995, but otherwise this file is perfect freeware.
*
*******************************************************************************
*/

#import "EnhanceMail.h"
#import "PGP.h"
#import "Preferences.h"
#import "SimpleString.h"
#import "XImageURL.h"
#import "regexp.h"
#import <ctype.h>

#define PGP_SIGNED "-----BEGIN PGP SIGNED MESSAGE-----"
#define PGP_ENCRYPTED "-----BEGIN PGP MESSAGE-----"

#define MSG_PGP_RICH NXLocalizedStringFromTableInBundle("Localizable", EnhanceBundle, "Only Plain Text messages are handled by PGP.", NULL, Error for attempt to PGP rich text messages)
///#define MSG_PGP_BCC NXLocalized StringFromTableInBundle("Localizable", EnhanceBundle, "Message will not be sent to Bcc: recipients when encrypting.", NULL, Alert for attempt to PGP encrypt with Bcc: recipients present)

#define MSG_CANCEL NXLoadLocalizedStringFromTableInBundle("Buttons", nil, "Cancel", NULL)
#define MSG_DELIVER NXLoadLocalizedStringFromTableInBundle("Buttons", nil, "Deliver Anyway", NULL)

/* [TRH] On second though I don't think it's a good idea to localize
   the header names after all, since it may defeat checks to prevent
   spoofed pgpheader. */
#define HDR_RESULT	"X-PGP-Result" //NXLocalized//StringFromTableInBundle("Localizable", EnhanceBundle, "X-PGP-Result", NULL, name of header reporting PGP decoding execution status)
#define SUCCESS_FMT	NXLocalizedStringFromTableInBundle("Localizable", EnhanceBundle, "Successful %s", NULL, printf format to report successful PGP execution; arg = %s: type)
#define FAILURE_FMT	NXLocalizedStringFromTableInBundle("Localizable", EnhanceBundle, "Failed %s (exit status %d)", NULL, printf format to report failed PGP execution; args = %s: type; %d: exit status)

#define PGP_DECRYPTION	NXLocalizedStringFromTableInBundle("Localizable", EnhanceBundle, "decryption", NULL, displayed in X-PGP_Result: when decrypting)
#define PGP_SIGCHECK	NXLocalizedStringFromTableInBundle("Localizable", EnhanceBundle, "signature verification", NULL, displayed in X-PGP-Result when checking signature)

#define PGP5_ERROR_RE	NXLocalizedStringFromTableInBundle("Localizable", EnhanceBundle, "PGP5_ERROR_REGEXP", "[Ee]rror|[Uu]nable|[Cc]annot|[Uu]nknown", reg exp to determine PGP 5 error status.  Please leave English in as fallback.)

#define HDR_NOTICE	"X-PGP-Notice" //NXLocalized//StringFromTableInBundle("Localizable", EnhanceBundle, "X-PGP-Notice", NULL, name of header reporting PGP status message if decoding successful)
#define HDR_WARNING	"X-PGP-Warning" //NXLocalized//StringFromTableInBundle("Localizable", EnhanceBundle, "X-PGP-Warning", NULL, name of header reporting nature of PGP decoding failure)

#define HDR_SPOOF	"X-PGP-Spoof" //NXLocalized//StringFromTableInBundle("Localizable", EnhanceBundle, "X-PGP-Spoof", NULL, name of header reporting PGP header spoof attempt)
#define MSG_SPOOFED	NXLocalizedStringFromTableInBundle("Localizable", EnhanceBundle, "Rats! Foiled again! Someone tried to spoof one or more X-PGP-* headers...", NULL, displayed in X-PGP-Spoof: header when a header spoofing attempt is detected)

static id mypgp=nil;

static regexp *keyrx=0, *addrrx=0;
static regexp *errorrx=0;

static const char *pgpheader[4]={0};

// result codes from PGP error panel.
enum { PGP_CANCEL, PGP_OK, PGP_REENTER };


@implementation EnhancePGP

+ new
{
   if (mypgp==nil) mypgp=[[self alloc] init];
   return mypgp;
}

- init
{
   char path[MAXPATHLEN+1];

   if ([EnhanceBundle getPath:path forResource:"EnhancePGPPanel" ofType:"nib"])
      [NXApp loadNibFile:path owner:self];

   if (keyrx==0)
      keyrx=regcomp("-----BEGIN PGP PUBLIC KEY BLOCK-----.*-----END PGP PUBLIC KEY BLOCK-----");

   if (addrrx==0)
      addrrx=regcomp("^[^<>]*<([^<> ]*)>[^<>]*$\n^ *([^() \t]*) *\\([^()]*\\) *$");

   /* Kludge: pgp 5.0 fails to return meaningful exit status on decryption,
      including bad pass phrase (Boo, Hiss).  So we are forced to resort to
      scanning the stderr output for ``meaningful'' error messages. */
   if (errorrx==0)
      errorrx=regcomp(PGP5_ERROR_RE);

   if (pgpheader[0]==0)
   {
      pgpheader[0] = HDR_RESULT;
#undef HDR_RESULT
#define HDR_RESULT pgpheader[0]
      pgpheader[1] = HDR_NOTICE;
#undef HDR_NOTICE
#define HDR_NOTICE pgpheader[1]
      pgpheader[2] = HDR_WARNING;
#undef HDR_WARNING
#define HDR_WARNING pgpheader[2]
      pgpheader[3] = HDR_SPOOF;
#undef HDR_SPOOF
#define HDR_SPOOF pgpheader[3]
   }
   return self;
}

- reenter:sender
{
   return [NXApp stopModal:PGP_REENTER];
}

- ok:sender
{
   return [NXApp stopModal:PGP_OK];
}

- cancel:sender
{
   return [NXApp stopModal:PGP_CANCEL];
}

- clobberPass
{
   const char *pass;
   char *c;

   if (!passField) return nil;
   pass=[passField stringValue];
   for(c=(char *)pass;*c;c++) *c='\0';
   [passField setStringValue:pass];
   return self;
}

- getUser:(const char **)user pass:(const char **)pass forEncoding:(BOOL)encode
{
   int ret;
   time_t curtime=time(0);

   if (curtime >= passTime ||
       curtime >= passEnterTime + EnhancePGPPassTimeout * 60 ||
       /* For security, the value of this option is remembered at the
          last time the password is set, and the remembered value is
	  checked here, so one is queried for a pass phrase even if
	  the option is turned off by a rogue user. */
       (encode && (EnhancePGPPassAskForSigning || prevPassAsk)))
   {
      [self clobberPass];
   }

   if (user && userField!=nil) *user=[userField stringValue];
   if (pass && passField!=nil) *pass=[passField stringValue];
   if (pass && *pass && **pass) return self;
   
   if (passPanel==nil) return nil;
   if (passField!=nil) [passField selectText:self];
   ret=[NXApp runModalFor:passPanel];
   [passPanel orderOut:self];
   if (ret==0) { user=pass=0; return nil; }
   if (user && userField) *user=[userField stringValue];
   if (pass && passField) *pass=[passField stringValue];

   passEnterTime = time(0);
   passTime = passEnterTime + EnhancePGPPassTimeout * 60;
   prevPassAsk = EnhancePGPPassAskForSigning;
   return self;
}

- (int)handleError:(const char *)err ok:(BOOL)defok
{
   int ret;
   const char *c;
   
   if ((errPanel==nil) || (errText==nil)) return PGP_CANCEL;

   for (c=err;*c;c++) if (*c=='\a') NXBeep();
   while (isspace(*err) || (*err == '\a')) err++;

   if (*err=='\0') return PGP_OK; // no error message, assume success.

   [errText setText:err];
   
   if (defok && (errDefaultButton!=nil) && (errNonDefaultButton!=nil))
   {
      SEL tmpact;
      char tmptitle[1024];
      
      tmpact=[errDefaultButton action];
      strcpy(tmptitle,[errDefaultButton title]);
      [errDefaultButton setAction:[errNonDefaultButton action]];
      [errDefaultButton setTitle:[errNonDefaultButton title]];
      [errNonDefaultButton setAction:tmpact];
      [errNonDefaultButton setTitle:tmptitle];
   }
   
   [errText scrollSelToVisible];
   ret=[NXApp runModalFor:errPanel];
   [errPanel orderOut:self];

   if (defok && (errDefaultButton!=nil) && (errNonDefaultButton!=nil))
   {
      SEL tmpact;
      char tmptitle[1024];
      
      tmpact=[errDefaultButton action];
      strcpy(tmptitle,[errDefaultButton title]);
      [errDefaultButton setAction:[errNonDefaultButton action]];
      [errDefaultButton setTitle:[errNonDefaultButton title]];
      [errNonDefaultButton setAction:tmpact];
      [errNonDefaultButton setTitle:tmptitle];
   }
   return ret;
}

/* Backward compatibility between pgp 2.x and pgp 5.x is obtained in a
   number of ways.  The most important is the PGPVersion preference that
   determines which version of PGP should be assumed.  (for now, this is
   an user-visible preference, until I figure out a way to determine this
   automatically.)

   The first argument of -runPGP:'s argv[] is now the invokation name of the
   pgp program, ie. it is passed as argv[0].  PGP 2.x should ignore this so
   it is safe to unconditionally hand this down.

   The password (if any) is now handed down to PGP in the recommended,
   least insecure, way.  At least for 2.6 and higher.  For 2.3 use the old,
   insecure, way of passing it via argv. */

- (int)runPGP:(SimpleString *)In to:(SimpleString *)Out error:(SimpleString *)Err
   args:(const char **)argv pass:(const char *)pass
{
   int inpipe[2],outpipe[2],errpipe[2];
   int ac,pid;
   const char **av;
   union wait status;

   if (access(EnhancePGPPath,X_OK) != 0)
   {
      [NXApp logError:"Missing pgp binary."];
      return -1;
   }
   
   for(ac=0;argv[ac];ac++);
   av=alloca(sizeof(*av)*(ac+9));
   ac=0;
   av[ac++]=*argv++;
   av[ac++]="+batchmode";
   av[ac++]="+force";
   av[ac++]="+encrypttoself=on";
   av[ac++]="+verbose=0";
   av[ac++]="-f";
   if (pass && *pass && (EnhancePGPVersion < PGP_VERSION_2_6))
   {
      av[ac++]="-z";
      av[ac++]=pass;
   }
   while (*argv) av[ac++]=*argv++;
   av[ac]=0;
   
   if ((pipe(inpipe)<0) || (pipe(outpipe)<0) || (pipe(errpipe)<0))
   {
      [NXApp logError:"Failure to create pipe pairs."];
      return -2;
   }
   
   if (!(pid=fork()))
   {
      extern char **_environ;
      char **ev = _environ;
      int ec;

      if (pass && *pass && (EnhancePGPVersion >= PGP_VERSION_2_6))
      {
	 // add PGPPASSFD=0 to child environment, to announce where
	 // pass phrase is to be found.
	 for (ec = 0;  ev[ec] != NULL;  ec++);
	 ev = alloca(sizeof(*ev)*(ec+2));
	 ec = 0;
	 ev[0] = "PGPPASSFD=0";
	 while ((ev[ec+1] = _environ[ec]) != NULL) ec++;
      }
      close(inpipe[1]);
      close(outpipe[0]);
      close(errpipe[0]);
      if (inpipe[0]  != 0) { dup2(inpipe[0],  0); close(inpipe[0]);  }
      if (outpipe[1] != 1) { dup2(outpipe[1], 1); close(outpipe[1]); }
      if (errpipe[1] != 2) { dup2(errpipe[1], 2); close(errpipe[1]); }
      ///execve(EnhancePGPPath, av, ev);
      // Ick! Mail.app does not have it in its symbol table.
      // So, instead, the next gross hack
      // (which we can get away with since this is in the child process):
      _environ = ev;
      execv(EnhancePGPPath, av);

      _exit(-1);
   }

   if (pid==-1)
   {
      [NXApp logError:"Failure to fork."];
      return -3;
   }
   
   close(inpipe[0]);
   close(outpipe[1]);
   close(errpipe[1]);

   if (pass && *pass)
   {
      if  (EnhancePGPVersion >= PGP_VERSION_2_6)
      {
	 // Write password as first thing on stdin.
	 write(inpipe[1], pass, strlen(pass));
	 write(inpipe[1], "\n", 1);
      }
      // Erase password immediately if user is paranoid.
      if (EnhancePGPPassTimeout == 0) [self clobberPass];
   }

   /* This is not quite proper, but if pgp works the way I think it does,
      it will work and be a lot shorter and simpler than the proper solution. */
   if (In!=nil)  [In writeFile:inpipe[1]];
   close(inpipe[1]);
   
   if (Out!=nil) [[Out empty] appendFile:outpipe[0]];
   close(outpipe[0]);
   
   if (Err!=nil) [[Err empty] appendFile:errpipe[0]];
   close(errpipe[0]);

   if (wait4(pid,&status,0,0)<0)
   {
      [NXApp logError:"PGP process disappeared."];
      return -4;
   }
   
   if (WIFSIGNALED(status))
   {
      [NXApp logError:"PGP process killed by signal (%d).", status.w_termsig];
      return -5;
   }
   
   return status.w_retcode;
}

- (BOOL)grabKey:(const char *)keyid
{
   int ac,ret;
   const char **av=0;
   char url[MAXPATHLEN+1];
   id s;

   if (!EnhanceRetrievePGPKeys || !EnhanceKeyServerURL) return NO;

   sprintf(url,"%s%s",EnhanceKeyServerURL,keyid);
   s=[[SimpleString alloc] init];
   if ([NXImage grabURL:url string:s]==NO) { s=[s free]; return NO; }
   if (regexec(keyrx,[s string])==NO) { s=[s free]; return NO; }

   // XXX Does not work for PGP 5 since pgpk is a separate executable,
   // not a link to PGP.
   av=alloca(sizeof(*av)*3);
   ac=0;
   av[ac++]="pgpk";
   av[ac++]= (EnhancePGPVersion >= PGP_VERSION_5) ? "-l" : "-ka";
   av[ac]=0;
   ret=[self runPGP:s to:nil error:nil args:av pass:NULL];
   s=[s free];
   return ret==0;
}

- (void)defeatSpoofedHeaders:(MailMessage *)mes
{
   unsigned i, count = sizeof(pgpheader) / sizeof(pgpheader[0]);
   MailHeaders *headers = [mes headers];
   BOOL spoofed = NO;

   for (i = 0;  i < count;  i++)
   {
      if ([headers hasKey:pgpheader[i]])
      {	 
      	 [headers removeKey:pgpheader[i]];
	 spoofed = YES;
      }
   }
   if (spoofed) [mes setHeaderKey:HDR_SPOOF value:MSG_SPOOFED];
}

static inline
BOOL pgpDecodeIsSuccessful(int status, BOOL decrypt, SimpleString *Out, SimpleString *Err)
{
   /* Kludge: pgp 5.0 fails to return meaningful exit status on decryption,
      including bad pass phrase (Boo, Hiss).  So we are forced to resort to
      scanning the stderr output for ``meaningful'' error messages. */
   if ((EnhancePGPVersion == PGP_VERSION_5_0) && (status == 0) && [Err length])
   {
      if (regexec(errorrx, [Err string])) return NO;
   }
   return (status == 0 || (decrypt && status == 1 && [Out length] >= 0));
   /* Latter is alternate PGP success, indicates absence of signature.
      -- of course this is ambiguous with PGP's use of exit status 1 as
      INVALID_FILE_ERROR (barf...) */
}

- (void)showDecodeStatus:(int)status
   out:(SimpleString *)Out err:(SimpleString *)Err
   decrypt:(BOOL)decrypt inMessage:(MailMessage *)mes
{
   BOOL success = pgpDecodeIsSuccessful(status, decrypt, Out, Err);
   char resultbuf[200];

   if ([Err length] > 0)
   {
      const char *key = (success) ? HDR_NOTICE : HDR_WARNING;
      char *str = [Err string];
      BOOL ring = NO;

      while (*str)
      {
	 if (*str == '\a')
	 {
	    ring = YES;
	    [Err delete:1 at:(str - [Err string])];
	 }
	 else if (*str++ == '\n')
	 {
	    char *t = str;

	    /* Collapse multiple newlines into a single one (ignoring
	       intervening whitespace), insert tab after newline. */
	    while (isspace(*t) || (*t == '\a'))
	    {
	       if (*t == '\a') ring = YES;
	       t++;
	    }

	    if (*t == '\0')
	    {
	       // remove trailing newline(s) + whitespace.
	       --str;
	       [Err delete:(t - str) at:(str - [Err string])];
	       break;
	    }

	    /* DON'T ignore whitespace following last newline! */
	    while (t[-1] != '\n') --t;

	    if ((t - str) == 0)
	    {
	       // single newline.
	       unsigned pos = (str - [Err string]);

	       [Err insertString:"\t" length:1 at:pos];
	       str = [Err string] + pos + 1; // might be relocated.
	    }
	    else
	    {
	       // overwrite extraneous newline with tab, then
	       // remove other extraneous newlines.
	       // !!! In place edit, depends on implementation of SimpleString!
	       *str++ = '\t';
	       [Err delete:(t - str) at:(str - [Err string])];
	    }
	 }
      }
      if (ring) NXBeep();

      [mes setHeaderKey:key value:[Err string]];
   }

   sprintf(resultbuf, (success) ? SUCCESS_FMT : FAILURE_FMT,
	   (decrypt) ? PGP_DECRYPTION : PGP_SIGCHECK, status);
   [mes setHeaderKey:HDR_RESULT value:resultbuf];
}

- (BOOL)decodePGP:(MailMessage *)mes
{
   id In,Out,Err;
   const char **av;
   const char *pass;
   int ret=0,ac;
   BOOL decrypt;
   BOOL success = YES;

   [self defeatSpoofedHeaders:mes];

   if ([mes isRichBody]) return NO;

   decrypt = (strncmp([mes body],PGP_ENCRYPTED,sizeof(PGP_ENCRYPTED)-1)==0);
   if (!decrypt
       && (strncmp([mes body],PGP_SIGNED,sizeof(PGP_SIGNED)-1)!=0))
   {
      return NO;
   }

   In=[[SimpleString alloc] init];
   [In appendString:[mes body] length:[mes bodyLength]];
   Out=[[SimpleString alloc] init];
   Err=[[SimpleString alloc] init];
   
   av=alloca(sizeof(*av)*(1+2+1));

 restart:
   pass = NULL;
   ac=0;
   av[ac++]="pgpv";

   if (decrypt)
   {
      const char *user;

      if ([self getUser:&user pass:&pass forEncoding:NO]==NO) goto end;
      if (user && *user) { av[ac++]="-u"; av[ac++]=user; }
      ///if (pass && *pass) { av[ac++]="-z"; av[ac++]=pass; }
   }
   av[ac]=0;

   ret=[self runPGP:In to:Out error:Err args:av pass:pass];
   success = pgpDecodeIsSuccessful(ret, decrypt, Out, Err);

   if (!success && decrypt)
   {
      switch ([self handleError:[Err string] ok:NO])
      {
      case PGP_CANCEL:
	 success = NO;
	 break;
      case PGP_OK:
	 success = YES;
	 break;
      case PGP_REENTER:
	 [self clobberPass];
	 goto restart;
      }
   }

   if ([Out length] > 0)
   {
      [mes setBody:[Out string] length:[Out length]];
   }
   [self showDecodeStatus:ret out:Out err:Err decrypt:decrypt inMessage:mes];

 end:
   [In free];
   [Out free];
   [Err free];

   return success;
}

- (BOOL)encodePGP:(MailMessage *)mes to:(StringList *)rcpt sign:(BOOL)sign encrypt:(BOOL)encrypt
{
   int ret=0, ac;
   const char **av;
   const char *pass;
   id In, Out,Err;

   if ([rcpt count]==0) encrypt=NO;
   if (!encrypt && !sign) return YES;
   if ([mes isRichBody])
   {
      NXBeep();
      return !NXRunAlertPanel(0,MSG_PGP_RICH,MSG_CANCEL,MSG_DELIVER,0);
   }
#if 0
   if (encrypt && [mes headerValueForKey:"Bcc"])
   {
      NXBeep();
      if (NXRunAlertPanel(0,MSG_PGP_BCC,MSG_CANCEL,MSG_DELIVER,0) != 0) return NO;
   }
#endif
   In=[[SimpleString alloc] init];
   [In appendString:[mes body] length:[mes bodyLength]];
   Out=[[SimpleString alloc] init];
   Err=[[SimpleString alloc] init];
   
   av=alloca(sizeof(*av)*(2*[rcpt count]+1+2+1+2+1+1));
   
 restart:
   pass = NULL;
   ac = 0;
   av[ac++] = "pgpe";
   av[ac++] = "-a";
   av[ac++] = "-t";

   if (sign)
   {
      const char *user;

      if (!encrypt) av[0] = "pgps";
      if (encrypt || EnhancePGPVersion < PGP_VERSION_5) av[ac++] = "-s";

      if ([self getUser:&user pass:&pass forEncoding:YES]==NO) goto end;
      if (user && *user) { av[ac++]="-u"; av[ac++]=user; }
      ///if (pass && *pass) { av[ac++]="-z"; av[ac++]=pass; }
   }
   
   if (encrypt)
   {
      int n;
      const char *r;
      char buf[1024];
#if 0
      const char *t, *bcc = [mes headerValueForKey:"Bcc"];
#endif

      if (EnhancePGPVersion < PGP_VERSION_5) av[ac++] = "-e";

      for(n=0;n<[rcpt count];n++)
      {
         r=[rcpt stringAt:n];
#if 0
	 // Ignore Bcc: recipients, so they won't show up in other people's
	 // PGP error messages.
	 if (bcc && (t = strstr(bcc, r)) &&
	     ((t == bcc) || isspace(t[-1]) || t[-1] == ',') &&
	     (*(t += strlen(r)) == '\0' || isspace(*t) || *t == ',')) continue;
#endif
         if (regexec(addrrx,r))
	 {
            regsub(addrrx,"\\1\\2",buf);
            r=strcpy(alloca(strlen(buf)+1),buf);
	 }
	 if (EnhancePGPVersion >= PGP_VERSION_5) av[ac++] = "-r";
	 av[ac++] = r;
      }
   }
   av[ac] = 0;

   ret = [self runPGP:In to:Out error:Err args:av pass:pass];

   if ((ret != 0) || ([Err length] > 0))
   {
      switch([self handleError:[Err string] ok:(ret==0)])
      {
      case PGP_CANCEL:
	 ret=-1;
	 break;
      case PGP_OK:
	 ret=0;
	 break;
      case PGP_REENTER:
	 [self clobberPass];
	 goto restart;
      }
   }
   
   if (ret==0) [mes setBody:[Out string] length:[Out length]];
   
 end:   
   [In free];
   [Out free];
   [Err free];
   
   return (ret==0);
}

@end // EnhancePGP
