/*
 * Copyright 2009-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <mongoc/mongoc-config.h>

#ifdef MONGOC_ENABLE_SSL_SECURE_TRANSPORT

#include <bson/bson.h>

#include <mongoc/mongoc-log.h>
#include <mongoc/mongoc-trace-private.h>
#include <mongoc/mongoc-ssl.h>
#include <mongoc/mongoc-stream-tls.h>
#include <mongoc/mongoc-stream-tls-private.h>
#include <mongoc/mongoc-secure-transport-private.h>
#include <mongoc/mongoc-stream-tls-secure-transport-private.h>

#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
#include <Security/SecKey.h>
#include <Security/SecureTransport.h>
#include <CommonCrypto/CommonDigest.h>
#include <Security/Security.h>
#include <Security/SecureTransport.h>
#include <CoreFoundation/CoreFoundation.h>
#include <common-string-private.h>

/* Jailbreak Darwin Private API */
/*
 * An alternative to using SecIdentityCreate is to use
 * SecIdentityCreateWithCertificate with a temporary keychain. However, doing so
 * leads to memory bugs. Unfortunately, using this private API seems to be the
 * best solution.
 */
SecIdentityRef
SecIdentityCreate (CFAllocatorRef allocator, SecCertificateRef certificate, SecKeyRef privateKey);

#undef MONGOC_LOG_DOMAIN
#define MONGOC_LOG_DOMAIN "stream-secure_transport"

char *
_mongoc_cfstringref_to_cstring (CFStringRef str)
{
   CFIndex length;
   CFStringEncoding encoding;
   CFIndex max_size;
   char *cs;

   if (!str) {
      return NULL;
   }

   if (CFGetTypeID (str) != CFStringGetTypeID ()) {
      return NULL;
   }

   length = CFStringGetLength (str);
   encoding = kCFStringEncodingASCII;
   max_size = CFStringGetMaximumSizeForEncoding (length, encoding) + 1;
   cs = bson_malloc ((size_t) max_size);

   if (CFStringGetCString (str, cs, max_size, encoding)) {
      return cs;
   }

   bson_free (cs);
   return NULL;
}

CFTypeRef
_mongoc_secure_transport_dict_get (CFArrayRef values, CFStringRef label)
{
   if (!values || CFGetTypeID (values) != CFArrayGetTypeID ()) {
      return NULL;
   }

   for (CFIndex i = 0; i < CFArrayGetCount (values); ++i) {
      CFStringRef item_label;
      CFDictionaryRef item = CFArrayGetValueAtIndex (values, i);

      if (CFGetTypeID (item) != CFDictionaryGetTypeID ()) {
         continue;
      }

      item_label = CFDictionaryGetValue (item, kSecPropertyKeyLabel);
      if (item_label && CFStringCompare (item_label, label, 0) == kCFCompareEqualTo) {
         return CFDictionaryGetValue (item, kSecPropertyKeyValue);
      }
   }

   return NULL;
}

static void
safe_release (CFTypeRef ref)
{
   if (ref) {
      CFRelease (ref);
   }
}


bool
_mongoc_secure_transport_import_pem (const char *filename,
                                     const char *passphrase,
                                     CFArrayRef *items,
                                     SecExternalItemType *type)
{
   SecExternalFormat format = kSecFormatPEMSequence;
   SecItemImportExportKeyParameters params = {0};
   SecTransformRef sec_transform = NULL;
   CFReadStreamRef read_stream = NULL;
   CFDataRef dataref = NULL;
   CFErrorRef error = NULL;
   CFURLRef url = NULL;
   OSStatus res;
   bool r = false;

   if (!filename) {
      TRACE ("%s", "No certificate provided");
      return false;
   }

   params.version = SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION;
   params.flags = 0;
   params.passphrase = NULL;
   params.alertTitle = NULL;
   params.alertPrompt = NULL;
   params.accessRef = NULL;
   params.keyUsage = NULL;
   params.keyAttributes = NULL;

   if (passphrase) {
      params.passphrase = CFStringCreateWithCString (kCFAllocatorDefault, passphrase, kCFStringEncodingUTF8);
   }

   url =
      CFURLCreateFromFileSystemRepresentation (kCFAllocatorDefault, (const UInt8 *) filename, strlen (filename), false);
   read_stream = CFReadStreamCreateWithFile (kCFAllocatorDefault, url);
   if (!CFReadStreamOpen (read_stream)) {
      MONGOC_ERROR ("Cannot find certificate in '%s', error reading file", filename);
      goto done;
   }

   sec_transform = SecTransformCreateReadTransformWithReadStream (read_stream);
   dataref = SecTransformExecute (sec_transform, &error);

   if (error) {
      CFStringRef str = CFErrorCopyDescription (error);
      MONGOC_ERROR (
         "Failed importing PEM '%s': %s", filename, CFStringGetCStringPtr (str, CFStringGetFastestEncoding (str)));

      CFRelease (str);
      goto done;
   }

   res = SecItemImport (dataref, CFSTR (".pem"), &format, type, 0, &params, NULL, items);

   if (res) {
      MONGOC_ERROR ("Failed importing PEM '%s' (code: %d)", filename, res);
      goto done;
   }

   r = true;

done:
   safe_release (dataref);
   safe_release (sec_transform);
   safe_release (read_stream);
   safe_release (url);
   safe_release (params.passphrase);

   return r;
}

static const char *
SecExternalItemType_to_string (SecExternalItemType value)
{
   switch (value) {
   case kSecItemTypeUnknown:
      return "kSecItemTypeUnknown";
   case kSecItemTypePrivateKey:
      return "kSecItemTypePrivateKey";
   case kSecItemTypePublicKey:
      return "kSecItemTypePublicKey";
   case kSecItemTypeSessionKey:
      return "kSecItemTypeSessionKey";
   case kSecItemTypeCertificate:
      return "kSecItemTypeCertificate";
   case kSecItemTypeAggregate:
      return "kSecItemTypeAggregate";
   default:
      return "Unknown";
   }
}

bool
mongoc_secure_transport_setup_certificate (mongoc_stream_tls_secure_transport_t *secure_transport,
                                           mongoc_ssl_opt_t *opt)
{
   bool success;
   CFArrayRef items;
   SecIdentityRef id;
   SecKeyRef key = NULL;
   SecCertificateRef cert = NULL;
   SecExternalItemType type = kSecItemTypeCertificate;

   if (!opt->pem_file) {
      TRACE ("%s", "No private key provided, the server won't be able to verify us");
      return false;
   }

   success = _mongoc_secure_transport_import_pem (opt->pem_file, opt->pem_pwd, &items, &type);
   if (!success) {
      /* caller will log an error */
      return false;
   }

   if (type != kSecItemTypeAggregate) {
      MONGOC_ERROR ("Cannot work with keys of type %s (%" PRIu32 "). Type is not supported",
                    SecExternalItemType_to_string (type),
                    type);
      CFRelease (items);
      return false;
   }

   for (CFIndex i = 0; i < CFArrayGetCount (items); ++i) {
      CFTypeID item_id = CFGetTypeID (CFArrayGetValueAtIndex (items, i));

      if (item_id == SecCertificateGetTypeID ()) {
         cert = (SecCertificateRef) CFArrayGetValueAtIndex (items, i);
      } else if (item_id == SecKeyGetTypeID ()) {
         key = (SecKeyRef) CFArrayGetValueAtIndex (items, i);
      }
   }

   if (!cert || !key) {
      MONGOC_ERROR ("Couldn't find valid private key");
      CFRelease (items);
      return false;
   }

   id = SecIdentityCreate (kCFAllocatorDefault, cert, key);
   secure_transport->my_cert = CFArrayCreateMutable (kCFAllocatorDefault, 2, &kCFTypeArrayCallBacks);

   CFArrayAppendValue (secure_transport->my_cert, id);
   CFArrayAppendValue (secure_transport->my_cert, cert);
   CFRelease (id);

   /*
    *  Secure Transport assumes the following:
    *    * The certificate references remain valid for the lifetime of the
    * session.
    *    * The identity specified in certRefs[0] is capable of signing.
    */
   success = !SSLSetCertificate (secure_transport->ssl_ctx_ref, secure_transport->my_cert);
   TRACE ("Setting client certificate %s", success ? "succeeded" : "failed");

   CFRelease (items);
   return success;
}

bool
mongoc_secure_transport_setup_ca (mongoc_stream_tls_secure_transport_t *secure_transport, mongoc_ssl_opt_t *opt)
{
   CFArrayRef items;
   SecExternalItemType type = kSecItemTypeCertificate;
   bool success;

   if (!opt->ca_file) {
      TRACE ("%s", "No CA provided, using defaults");
      return false;
   }

   success = _mongoc_secure_transport_import_pem (opt->ca_file, NULL, &items, &type);

   if (!success) {
      MONGOC_ERROR ("Cannot load Certificate Authorities from file \'%s\'", opt->ca_file);
      return false;
   }

   if (type == kSecItemTypeAggregate) {
      CFMutableArrayRef anchors = CFArrayCreateMutable (kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);

      for (CFIndex i = 0; i < CFArrayGetCount (items); ++i) {
         CFTypeID item_id = CFGetTypeID (CFArrayGetValueAtIndex (items, i));

         if (item_id == SecCertificateGetTypeID ()) {
            CFArrayAppendValue (anchors, CFArrayGetValueAtIndex (items, i));
         }
      }
      secure_transport->anchors = anchors;
      CFRelease (items);
   } else if (type == kSecItemTypeCertificate) {
      secure_transport->anchors = items;
   } else {
      CFRelease (items);
   }

   /* This should be SSLSetCertificateAuthorities But the /TLS/ tests fail
    * when it is */
   success = !SSLSetTrustedRoots (secure_transport->ssl_ctx_ref, secure_transport->anchors, true);
   TRACE ("Setting certificate authority %s (%s)", success ? "succeeded" : "failed", opt->ca_file);
   return success;
}

OSStatus
mongoc_secure_transport_read (SSLConnectionRef connection, void *data, size_t *data_length)
{
   mongoc_stream_tls_t *tls = (mongoc_stream_tls_t *) connection;
   ssize_t length;
   ENTRY;

   errno = 0;
   /* 4 arguments is *min_bytes* -- This is not a negotiation.
    * Secure Transport wants all or nothing. We must continue reading until
    * we get this amount, or timeout */
   length = mongoc_stream_read (tls->base_stream, data, *data_length, *data_length, tls->timeout_msec);

   if (length > 0) {
      *data_length = length;
      RETURN (noErr);
   }

   if (length == 0) {
      RETURN (errSSLClosedGraceful);
   }

   switch (errno) {
   case ENOENT:
      RETURN (errSSLClosedGraceful);
      break;
   case ECONNRESET:
      RETURN (errSSLClosedAbort);
      break;
   case EAGAIN:
      RETURN (errSSLWouldBlock);
      break;
   default:
      RETURN (-36); /* ioErr */
      break;
   }
}

OSStatus
mongoc_secure_transport_write (SSLConnectionRef connection, const void *data, size_t *data_length)
{
   mongoc_stream_tls_t *tls = (mongoc_stream_tls_t *) connection;
   ssize_t length;
   ENTRY;

   errno = 0;
   length = mongoc_stream_write (tls->base_stream, (void *) data, *data_length, tls->timeout_msec);

   if (length >= 0) {
      *data_length = length;
      RETURN (noErr);
   }

   switch (errno) {
   case EAGAIN:
      RETURN (errSSLWouldBlock);
   default:
      RETURN (-36); /* ioErr */
   }
}

void
CFReleaseSafe (CFTypeRef cf)
{
   if (cf != NULL) {
      CFRelease (cf);
   }
}

#endif
