/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include "pkix/test/pkixtestutil.h"
#include "pkix/test/pkixtestnss.h"

#include <limits>

#include "cryptohi.h"
#include "keyhi.h"
#include "nss.h"
#include "pk11pqg.h"
#include "pk11pub.h"
#include "nss_scoped_ptrs.h"
#include "pkix/pkixnss.h"
#include "pkix/pkixder.h"
#include "pkix/pkixutil.h"
#include "prinit.h"
#include "secerr.h"
#include "secitem.h"

namespace mozilla { namespace pkix { namespace test {

namespace {

TestKeyPair* GenerateKeyPairInner();

void
InitNSSIfNeeded()
{
  if (NSS_NoDB_Init(nullptr) != SECSuccess) {
    abort();
  }
}

static ScopedTestKeyPair reusedKeyPair;

PRStatus
InitReusedKeyPair()
{
  InitNSSIfNeeded();
  reusedKeyPair.reset(GenerateKeyPairInner());
  return reusedKeyPair ? PR_SUCCESS : PR_FAILURE;
}

class NSSTestKeyPair final : public TestKeyPair
{
public:
  NSSTestKeyPair(const TestPublicKeyAlgorithm& aPublicKeyAlg,
                 const ByteString& spk,
                 const ByteString& aEncryptedPrivateKey,
                 const ByteString& aEncryptionAlgorithm,
                 const ByteString& aEncryptionParams)
    : TestKeyPair(aPublicKeyAlg, spk)
    , encryptedPrivateKey(aEncryptedPrivateKey)
    , encryptionAlgorithm(aEncryptionAlgorithm)
    , encryptionParams(aEncryptionParams)
  {
  }

  Result SignData(const ByteString& tbs,
                  const TestSignatureAlgorithm& signatureAlgorithm,
                  /*out*/ ByteString& signature) const override
  {
    SECOidTag oidTag;
    if (signatureAlgorithm.publicKeyAlg == RSA_PKCS1()) {
      switch (signatureAlgorithm.digestAlg) {
        case TestDigestAlgorithmID::MD2:
          oidTag = SEC_OID_PKCS1_MD2_WITH_RSA_ENCRYPTION;
          break;
        case TestDigestAlgorithmID::MD5:
          oidTag = SEC_OID_PKCS1_MD5_WITH_RSA_ENCRYPTION;
          break;
        case TestDigestAlgorithmID::SHA1:
          oidTag = SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION;
          break;
        case TestDigestAlgorithmID::SHA224:
          oidTag = SEC_OID_PKCS1_SHA224_WITH_RSA_ENCRYPTION;
          break;
        case TestDigestAlgorithmID::SHA256:
          oidTag = SEC_OID_PKCS1_SHA256_WITH_RSA_ENCRYPTION;
          break;
        case TestDigestAlgorithmID::SHA384:
          oidTag = SEC_OID_PKCS1_SHA384_WITH_RSA_ENCRYPTION;
          break;
        case TestDigestAlgorithmID::SHA512:
          oidTag = SEC_OID_PKCS1_SHA512_WITH_RSA_ENCRYPTION;
          break;
        MOZILLA_PKIX_UNREACHABLE_DEFAULT_ENUM
      }
    } else {
      abort();
    }

    ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
    if (!slot) {
      return MapPRErrorCodeToResult(PR_GetError());
    }
    SECItem encryptedPrivateKeyInfoItem = {
      siBuffer,
      const_cast<uint8_t*>(encryptedPrivateKey.data()),
      static_cast<unsigned int>(encryptedPrivateKey.length())
    };
    SECItem encryptionAlgorithmItem = {
      siBuffer,
      const_cast<uint8_t*>(encryptionAlgorithm.data()),
      static_cast<unsigned int>(encryptionAlgorithm.length())
    };
    SECItem encryptionParamsItem = {
      siBuffer,
      const_cast<uint8_t*>(encryptionParams.data()),
      static_cast<unsigned int>(encryptionParams.length())
    };
    SECKEYEncryptedPrivateKeyInfo encryptedPrivateKeyInfo = {
      nullptr,
      { encryptionAlgorithmItem, encryptionParamsItem },
      encryptedPrivateKeyInfoItem
    };
    SECItem passwordItem = { siBuffer, nullptr, 0 };
    SECItem publicValueItem = {
      siBuffer,
      const_cast<uint8_t*>(subjectPublicKey.data()),
      static_cast<unsigned int>(subjectPublicKey.length())
    };
    SECKEYPrivateKey* privateKey;
    // This should always be an RSA key (we'll have aborted above if we're not
    // doing an RSA signature).
    if (PK11_ImportEncryptedPrivateKeyInfoAndReturnKey(
          slot.get(), &encryptedPrivateKeyInfo, &passwordItem, nullptr,
          &publicValueItem, false, false, rsaKey, KU_ALL, &privateKey,
          nullptr) != SECSuccess) {
      return MapPRErrorCodeToResult(PR_GetError());
    }
    ScopedSECKEYPrivateKey scopedPrivateKey(privateKey);
    SECItem signatureItem;
    if (SEC_SignData(&signatureItem, tbs.data(),
                     static_cast<int>(tbs.length()),
                     scopedPrivateKey.get(), oidTag) != SECSuccess) {
      return MapPRErrorCodeToResult(PR_GetError());
    }
    signature.assign(signatureItem.data, signatureItem.len);
    SECITEM_FreeItem(&signatureItem, false);
    return Success;
  }

  TestKeyPair* Clone() const override
  {
    return new (std::nothrow) NSSTestKeyPair(publicKeyAlg,
                                             subjectPublicKey,
                                             encryptedPrivateKey,
                                             encryptionAlgorithm,
                                             encryptionParams);
  }

private:
  const ByteString encryptedPrivateKey;
  const ByteString encryptionAlgorithm;
  const ByteString encryptionParams;
};

} // namespace

// This private function is also used by Gecko's PSM test framework
// (OCSPCommon.cpp).
TestKeyPair* CreateTestKeyPair(const TestPublicKeyAlgorithm publicKeyAlg,
                               const ScopedSECKEYPublicKey& publicKey,
                               const ScopedSECKEYPrivateKey& privateKey)
{
  ScopedCERTSubjectPublicKeyInfo
    spki(SECKEY_CreateSubjectPublicKeyInfo(publicKey.get()));
  if (!spki) {
    return nullptr;
  }
  SECItem spkDER = spki->subjectPublicKey;
  DER_ConvertBitString(&spkDER); // bits to bytes
  ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
  if (!slot) {
    return nullptr;
  }
  // Because NSSTestKeyPair isn't tracked by XPCOM and won't otherwise be aware
  // of shutdown, we don't have a way to release NSS resources at the
  // appropriate time. To work around this, NSSTestKeyPair doesn't hold on to
  // NSS resources. Instead, we export the generated private key part as an
  // encrypted blob (with an empty password and fairly lame encryption). When we
  // need to use it (e.g. to sign something), we decrypt it and create a
  // temporary key object.
  SECItem passwordItem = { siBuffer, nullptr, 0 };
  ScopedSECKEYEncryptedPrivateKeyInfo encryptedPrivateKey(
    PK11_ExportEncryptedPrivKeyInfo(
      slot.get(), SEC_OID_PKCS12_V2_PBE_WITH_SHA1_AND_3KEY_TRIPLE_DES_CBC,
      &passwordItem, privateKey.get(), 1, nullptr));
  if (!encryptedPrivateKey) {
    return nullptr;
  }

  return new (std::nothrow) NSSTestKeyPair(
    publicKeyAlg,
    ByteString(spkDER.data, spkDER.len),
    ByteString(encryptedPrivateKey->encryptedData.data,
               encryptedPrivateKey->encryptedData.len),
    ByteString(encryptedPrivateKey->algorithm.algorithm.data,
               encryptedPrivateKey->algorithm.algorithm.len),
    ByteString(encryptedPrivateKey->algorithm.parameters.data,
               encryptedPrivateKey->algorithm.parameters.len));
}

namespace {

TestKeyPair*
GenerateKeyPairInner()
{
  ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
  if (!slot) {
    abort();
  }
  PK11RSAGenParams params;
  params.keySizeInBits = 2048;
  params.pe = 65537;

  // Bug 1012786: PK11_GenerateKeyPair can fail if there is insufficient
  // entropy to generate a random key. Attempting to add some entropy and
  // retrying appears to solve this issue.
  for (uint32_t retries = 0; retries < 10; retries++) {
    SECKEYPublicKey* publicKeyTemp = nullptr;
    ScopedSECKEYPrivateKey
      privateKey(PK11_GenerateKeyPair(slot.get(), CKM_RSA_PKCS_KEY_PAIR_GEN,
                                      &params, &publicKeyTemp, false, true,
                                      nullptr));
    ScopedSECKEYPublicKey publicKey(publicKeyTemp);
    if (privateKey) {
      return CreateTestKeyPair(RSA_PKCS1(), publicKey, privateKey);
    }

    assert(!publicKeyTemp);

    if (PR_GetError() != SEC_ERROR_PKCS11_FUNCTION_FAILED) {
      break;
    }

    // Since these keys are only for testing, we don't need them to be good,
    // random keys.
    // https://xkcd.com/221/
    static const uint8_t RANDOM_NUMBER[] = { 4, 4, 4, 4, 4, 4, 4, 4 };
    if (PK11_RandomUpdate(
          const_cast<void*>(reinterpret_cast<const void*>(RANDOM_NUMBER)),
          sizeof(RANDOM_NUMBER)) != SECSuccess) {
      break;
    }
  }

  abort();
}

} // namespace

TestKeyPair*
GenerateKeyPair()
{
  InitNSSIfNeeded();
  return GenerateKeyPairInner();
}

TestKeyPair*
CloneReusedKeyPair()
{
  static PRCallOnceType initCallOnce;
  if (PR_CallOnce(&initCallOnce, InitReusedKeyPair) != PR_SUCCESS) {
    abort();
  }
  assert(reusedKeyPair);
  return reusedKeyPair->Clone();
}

TestKeyPair*
GenerateDSSKeyPair()
{
  InitNSSIfNeeded();

  ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
  if (!slot) {
    return nullptr;
  }

  ByteString p(DSS_P());
  ByteString q(DSS_Q());
  ByteString g(DSS_G());

  static const PQGParams PARAMS = {
    nullptr,
    { siBuffer,
      const_cast<uint8_t*>(p.data()),
      static_cast<unsigned int>(p.length())
    },
    { siBuffer,
      const_cast<uint8_t*>(q.data()),
      static_cast<unsigned int>(q.length())
    },
    { siBuffer,
      const_cast<uint8_t*>(g.data()),
      static_cast<unsigned int>(g.length())
    }
  };

  SECKEYPublicKey* publicKeyTemp = nullptr;
  ScopedSECKEYPrivateKey
    privateKey(PK11_GenerateKeyPair(slot.get(), CKM_DSA_KEY_PAIR_GEN,
                                    const_cast<PQGParams*>(&PARAMS),
                                    &publicKeyTemp, false, true, nullptr));
  if (!privateKey) {
    return nullptr;
  }
  ScopedSECKEYPublicKey publicKey(publicKeyTemp);
  return CreateTestKeyPair(DSS(), publicKey, privateKey);
}

Result
TestVerifyECDSASignedDigest(const SignedDigest& signedDigest,
                            Input subjectPublicKeyInfo)
{
  InitNSSIfNeeded();
  return VerifyECDSASignedDigestNSS(signedDigest, subjectPublicKeyInfo,
                                    nullptr);
}

Result
TestVerifyRSAPKCS1SignedDigest(const SignedDigest& signedDigest,
                               Input subjectPublicKeyInfo)
{
  InitNSSIfNeeded();
  return VerifyRSAPKCS1SignedDigestNSS(signedDigest, subjectPublicKeyInfo,
                                       nullptr);
}

Result
TestDigestBuf(Input item,
              DigestAlgorithm digestAlg,
              /*out*/ uint8_t* digestBuf,
              size_t digestBufLen)
{
  InitNSSIfNeeded();
  return DigestBufNSS(item, digestAlg, digestBuf, digestBufLen);
}

} } } // namespace mozilla::pkix::test
