/* NSData+SMIME.m - this file is part of SOGo
 *
 * Copyright (C) 2017-2019 Inverse inc.
 *
 * This file is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This file is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSString.h>

#import <NGExtensions/NGBase64Coding.h>
#import <NGExtensions/NGQuotedPrintableCoding.h>
#import <NGExtensions/NSString+Encoding.h>
#import <NGExtensions/NSObject+Logs.h>

#import <NGMail/NGMimeMessageParser.h>

#if defined(HAVE_OPENSSL) || defined(HAVE_GNUTLS)
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/pkcs7.h>
#include <openssl/pkcs12.h>
#include <openssl/pem.h>
#endif

#import <SOGo/NSString+Utilities.h>
#import "NSData+SMIME.h"

@implementation NSData (SOGoMailSMIME)

//
//
//
- (NSData *) signUsingCertificateAndKey: (NSData *) theData
{
  NSData *output = NULL;

  BIO *tbio = NULL, *sbio = NULL, *obio = NULL;
  X509 *scert = NULL;
  X509 *link = NULL;
  STACK_OF(X509) *chain = NULL;
  EVP_PKEY *skey = NULL;
  PKCS7 *p7 = NULL;
  BUF_MEM *bptr;
  
  unsigned int len, slen;
  const char* bytes;
  const char* sbytes;

  int flags = PKCS7_STREAM | PKCS7_DETACHED | PKCS7_CRLFEOL;
  
  OpenSSL_add_all_algorithms();
  ERR_load_crypto_strings();
  
  bytes = [theData bytes];
  len = [theData length];
  tbio = BIO_new_mem_buf((void *)bytes, len);

  scert = PEM_read_bio_X509(tbio, NULL, 0, NULL);

  if (!scert)
    {
      NSLog(@"FATAL: failed to read certificate for signing.");
      goto cleanup;
    }
  
  chain = sk_X509_new_null();
  while (link = PEM_read_bio_X509_AUX(tbio, NULL, 0, NULL))
    sk_X509_unshift(chain, link);

  BIO_reset(tbio);
  
  skey = PEM_read_bio_PrivateKey(tbio, NULL, 0, NULL);

  if (!skey)
    {
      NSLog(@"FATAL: failed to read private key for signing.");
      goto cleanup;
    }
  
  // We sign
  sbytes = [self bytes];
  slen = [self length];
  sbio = BIO_new_mem_buf((void *)sbytes, slen);
  p7 = PKCS7_sign(scert, skey, (sk_X509_num(chain) > 0) ? chain : NULL, sbio, flags);

  if (!p7)
    {
      NSLog(@"FATAL: failed to sign message.");
      goto cleanup;
    }
  
  // We output
  obio = BIO_new(BIO_s_mem());
  SMIME_write_PKCS7(obio, p7, sbio, flags);
  BIO_get_mem_ptr(obio, &bptr);

  output = [NSData dataWithBytes: bptr->data  length: bptr->length];

 cleanup:
  PKCS7_free(p7);
  sk_X509_pop_free(chain, X509_free);
  X509_free(scert);     
  BIO_free(tbio);
  BIO_free(sbio);
  BIO_free(obio);
  
  return output;
}

//
//
//
- (NSData *) encryptUsingCertificate: (NSData *) theData
{
  NSData *output = NULL;

  BUF_MEM *bptr = NULL;
  BIO *tbio = NULL, *sbio = NULL, *obio = NULL;
  X509 *rcert = NULL;
  PKCS7 *p7 = NULL;
  STACK_OF(X509) *recips = NULL;
  
  unsigned int len, slen;
  const char* bytes;
  const char* sbytes;

  int flags = PKCS7_STREAM;
  
  OpenSSL_add_all_algorithms();
  ERR_load_crypto_strings();

  bytes = [theData bytes];
  len = [theData length];
  tbio = BIO_new_mem_buf((void *)bytes, len);
  if (!tbio)
    {
      NSLog(@"FATAL: unable to allocate BIO memory");
      goto cleanup;
    }

  // Grab the last certificate in case it's chained
  rcert = NULL;
  while (PEM_read_bio_X509(tbio, &rcert, 0, NULL) != NULL);

  if (!rcert)
    {
      NSLog(@"FATAL: unable to read certificate for encryption");
      goto cleanup;
    }
  
  recips = sk_X509_new_null();
  
  if (!recips || !sk_X509_push(recips, rcert))
    {
      NSLog(@"FATAL: unable to push certificate into stack");
      goto cleanup;
    }

  rcert = NULL;

  // Get the bytes to encrypt
  sbytes = [self bytes];
  slen = [self length];
  sbio = BIO_new_mem_buf((void *)sbytes, slen);

  // Encrypt
  p7 = PKCS7_encrypt(recips, sbio, EVP_des_ede3_cbc(), flags);

  if (!p7)
    {
      NSLog(@"FATAL: unable to encrypt message");
      goto cleanup;
    }

  // We output the S/MIME encrypted message
  obio = BIO_new(BIO_s_mem());
  if (!SMIME_write_PKCS7(obio, p7, sbio, flags))
    {
      NSLog(@"FATAL: unable to write PKCS7 output");
      goto cleanup;
    }

  BIO_get_mem_ptr(obio, &bptr);
  
  output = [NSData dataWithBytes: bptr->data  length: bptr->length];

 cleanup:
  PKCS7_free(p7);
  X509_free(rcert);        
  BIO_free(tbio);
  BIO_free(sbio);
  BIO_free(obio);

  return output;
}

//
//
//
- (NSData *) decryptUsingCertificate: (NSData *) theData
{
  NSData *output = NULL;

  BIO *tbio, *sbio, *obio;
  BUF_MEM *bptr;
  X509 *scert = NULL;
  EVP_PKEY *skey = NULL;
  PKCS7 *p7 = NULL;
  
  unsigned int len, slen;
  const char* bytes;
  const char* sbytes;
  
  OpenSSL_add_all_algorithms();
  ERR_load_crypto_strings();

  bytes = [theData bytes];
  len = [theData length];
  tbio = BIO_new_mem_buf((void *)bytes, len);

  // Grab the last certificate in case it's chained
  scert = NULL;
  while (PEM_read_bio_X509(tbio, &scert, 0, NULL) != NULL);

  if (!scert)
    {
      NSLog(@"FATAL: could not read certificate for decryption");
      goto cleanup;
    } 
  
  BIO_reset(tbio);

  skey = PEM_read_bio_PrivateKey(tbio, NULL, 0, NULL);

  if (!skey)
    {
      NSLog(@"FATAL: could not read private key for decryption");
      goto cleanup;
    } 
  
  sbytes = [self bytes];
  slen = [self length];
  sbio = BIO_new_mem_buf((void *)sbytes, slen);

  p7 = SMIME_read_PKCS7(sbio, NULL);

  if (!p7)
    {
      NSLog(@"FATAL: could not read the content to be decrypted");
      goto cleanup;
    }

  // We output the S/MIME encrypted message
  obio = BIO_new(BIO_s_mem());
  
  if (!PKCS7_decrypt(p7, skey, scert, obio, 0))
    {
      NSLog(@"FATAL: could not decrypt content");
      goto cleanup;
    }
  
  BIO_get_mem_ptr(obio, &bptr);
  
  output = [NSData dataWithBytes: bptr->data  length: bptr->length];

 cleanup:
  PKCS7_free(p7);
  X509_free(scert); 
  BIO_free(sbio);
  BIO_free(tbio);
  BIO_free(obio);
  
  return output;
}

//
//
//
- (NGMimeMessage *) messageFromEncryptedDataAndCertificate: (NSData *) theCertificate
{
  NGMimeMessageParser *parser;
  NGMimeMessage *message;
  NSData *decryptedData; 
  NGMimeType *contentType;
  NSString *type, *subtype, *smimetype;
 
  decryptedData = [self decryptUsingCertificate: theCertificate];
  parser = [[NGMimeMessageParser alloc] init];
  message = [parser parsePartFromData: decryptedData];

  // Extract contents if the encrypted messages contains opaque signed data
  contentType = [message contentType];
  type = [[contentType type] lowercaseString];
  subtype = [[contentType subType] lowercaseString];
  if ([type isEqualToString: @"application"])
    {
      if ([subtype isEqualToString: @"x-pkcs7-mime"] ||
          [subtype isEqualToString: @"pkcs7-mime"])
	{
	  smimetype = [[contentType valueOfParameter: @"smime-type"] lowercaseString];
	  if ([smimetype isEqualToString: @"signed-data"])
	    {
	      message = [decryptedData messageFromOpaqueSignedData];
	    }
	}
    }

  RELEASE(parser);

  return message;
}

- (NSData *) embeddedContent
{
  NSData *output = NULL;

  BIO *sbio, *obio;
  BUF_MEM *bptr;
  PKCS7 *p7 = NULL;

  sbio = BIO_new_mem_buf((void *)[self bytes], [self length]);

  p7 = SMIME_read_PKCS7(sbio, NULL);

  if (!p7)
    {
      NSLog(@"FATAL: could not read the signature");
      goto cleanup;
    }

  // We output the S/MIME encrypted message
  obio = BIO_new(BIO_s_mem());

  if (!PKCS7_verify(p7, NULL, NULL, NULL, obio, PKCS7_NOVERIFY|PKCS7_NOSIGS))
    {
      NSLog(@"FATAL: could not extract content");
      goto cleanup;
    }

  BIO_get_mem_ptr(obio, &bptr);

  output = [NSData dataWithBytes: bptr->data  length: bptr->length];

 cleanup:
  PKCS7_free(p7);
  BIO_free(sbio);
  BIO_free(obio);

  return output;
}

//
//
//
- (NGMimeMessage *) messageFromOpaqueSignedData
{
  NGMimeMessageParser *parser;
  NGMimeMessage *message;
  NSData *extractedData;

  extractedData = [self embeddedContent];
  parser = [[NGMimeMessageParser alloc] init];
  message = [parser parsePartFromData: extractedData];
  RELEASE(parser);

  return message;
}

//
//
//
- (NSData *) convertPKCS12ToPEMUsingPassword: (NSString *) thePassword
{
  NSData *output = NULL;

  BIO *ibio, *obio;
  EVP_PKEY *pkey;
  BUF_MEM *bptr;
  PKCS12 *p12;
  X509 *cert;

  const char* bytes;
  int i, len;
  
  STACK_OF(X509) *ca = NULL;

  OpenSSL_add_all_algorithms();
  ERR_load_crypto_strings();

  bytes = [self bytes];
  len = [self length];
  ibio = BIO_new_mem_buf((void *)bytes, len);
  
  p12 = d2i_PKCS12_bio(ibio, NULL);

  if (!p12)
    {
      NSLog(@"FATAL: could not read PKCS12 content");
      goto cleanup;
    }

  if (!PKCS12_parse(p12, [thePassword UTF8String], &pkey, &cert, &ca))
    {
      NSLog(@"FATAL");
      return nil;
    }
  
  // We output everything in PEM
  obio = BIO_new(BIO_s_mem());

  // TODO: support protecting the private key with a PEM passphrase
  if (pkey)
    {
      PEM_write_bio_PrivateKey(obio, pkey, NULL, NULL, 0, NULL, NULL);
    }

  if (cert)
    {
      PEM_write_bio_X509(obio, cert);
    }
  
  if (ca && sk_X509_num(ca))
    {
      for (i = 0; i < sk_X509_num(ca); i++)
        PEM_write_bio_X509_AUX(obio, sk_X509_value(ca, i));
    }

  BIO_get_mem_ptr(obio, &bptr);

  output = [NSData dataWithBytes: bptr->data  length: bptr->length];

 cleanup:
  PKCS12_free(p12);
  BIO_free(ibio);  
  BIO_free(obio);  
  
  return output;
}

//
//
//
- (NSData *) signersFromPKCS7
{
  NSData *output = NULL;

  STACK_OF(X509) *certs = NULL;
  BIO *ibio, *obio;
  BUF_MEM *bptr;
  PKCS7 *p7;

  const char* bytes;
  int i, len;
  
  OpenSSL_add_all_algorithms();
  ERR_load_crypto_strings();

  bytes = [self bytes];
  len = [self length];
  ibio = BIO_new_mem_buf((void *)bytes, len);
  
  p7 = d2i_PKCS7_bio(ibio, NULL);

  if (!p7)
    {
      NSLog(@"FATAL: could not read PKCS7 content");
      goto cleanup;
    }
  
  // We output everything in PEM
  obio = BIO_new(BIO_s_mem());

  certs = PKCS7_get0_signers(p7, NULL, 0);
  if (certs != NULL)
    {
      X509 *x;

      for (i = 0; i < sk_X509_num(certs); i++)
        {
          x = sk_X509_value(certs, i);
          PEM_write_bio_X509(obio, x);
          BIO_puts(obio, "\n");
        }
    }

  BIO_get_mem_ptr(obio, &bptr);

  output = [NSData dataWithBytes: bptr->data  length: bptr->length];

 cleanup:
  PKCS7_free(p7);
  BIO_free(ibio);  
  BIO_free(obio);  
  
  return output;
}

/**
 * Extract usefull information from PEM certificate
 */
- (NSDictionary *) certificateDescription
{
  NSDictionary *data;
  BIO *bio;
  X509 *x;

  data = nil;
  OpenSSL_add_all_algorithms();
  bio = BIO_new_mem_buf((void *) [self bytes], [self length]);

  // Grab the last certificate in case it's chained
  x = NULL;
  while (PEM_read_bio_X509(bio, &x, 0, NULL) != NULL);

  if (x)
    {
      NSString *subject, *issuer;
      char p[1024];
      BIO *buf;

      memset(p, 0, 1024);
      buf = BIO_new(BIO_s_mem());
      X509_NAME_print_ex(buf, X509_get_subject_name(x), 0,
                         ASN1_STRFLGS_ESC_CTRL | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_LN);
      BIO_read(buf, p, 1024);
      subject = [NSString stringWithUTF8String: p];
      BIO_free(buf);

      memset(p, 0, 1024);
      buf = BIO_new(BIO_s_mem());
      X509_NAME_print_ex(buf, X509_get_issuer_name(x), 0,
                             ASN1_STRFLGS_ESC_CTRL | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_LN);
      BIO_read(buf, p, 1024);
      issuer = [NSString stringWithUTF8String: p];
      BIO_free(buf);

      data = [NSDictionary dictionaryWithObjectsAndKeys:
                             [subject componentsFromMultilineDN], @"subject",
                           [issuer componentsFromMultilineDN], @"issuer",
                           nil];
    }
  else
    {
      NSString *error;
      const char* sslError;
      int err;

      err = ERR_get_error();
      ERR_load_crypto_strings();
      sslError = ERR_reason_error_string(err);
      error = [NSString stringWithUTF8String: sslError];
      NSLog(@"FATAL: failed to read certificate: %@", error);
    }

  return data;
}

@end
