// 7zAes.cpp

#include "StdAfx.h"

#include "../../../C/CpuArch.h"
#include "../../../C/Sha256.h"

#include "../../Common/ComTry.h"
#include "../../Common/MyBuffer2.h"

#ifndef _7ZIP_ST
#include "../../Windows/Synchronization.h"
#endif

#include "../Common/StreamUtils.h"

#include "7zAes.h"
#include "MyAes.h"

#ifndef EXTRACT_ONLY
#include "RandGen.h"
#endif

namespace NCrypto {
namespace N7z {

static const unsigned k_NumCyclesPower_Supported_MAX = 24;

bool CKeyInfo::IsEqualTo(const CKeyInfo &a) const
{
  if (SaltSize != a.SaltSize || NumCyclesPower != a.NumCyclesPower)
    return false;
  for (unsigned i = 0; i < SaltSize; i++)
    if (Salt[i] != a.Salt[i])
      return false;
  return (Password == a.Password);
}

void CKeyInfo::CalcKey()
{
  if (NumCyclesPower == 0x3F)
  {
    unsigned pos;
    for (pos = 0; pos < SaltSize; pos++)
      Key[pos] = Salt[pos];
    for (unsigned i = 0; i < Password.Size() && pos < kKeySize; i++)
      Key[pos++] = Password[i];
    for (; pos < kKeySize; pos++)
      Key[pos] = 0;
  }
  else
  {
    const unsigned kUnrPow = 6;
    const UInt32 numUnroll = (UInt32)1 << (NumCyclesPower <= kUnrPow ? (unsigned)NumCyclesPower : kUnrPow);

    const size_t bufSize = 8 + SaltSize + Password.Size();
    const size_t unrollSize = bufSize * numUnroll;

    // MY_ALIGN (16)
    // CSha256 sha;
    CAlignedBuffer sha(sizeof(CSha256) + unrollSize + bufSize * 2);
    Byte *buf = sha + sizeof(CSha256);

    memcpy(buf, Salt, SaltSize);
    memcpy(buf + SaltSize, Password, Password.Size());
    memset(buf + bufSize - 8, 0, 8);
    
    Sha256_Init((CSha256 *)(void *)(Byte *)sha);
    
    {
      {
        Byte *dest = buf;
        for (UInt32 i = 1; i < numUnroll; i++)
        {
          dest += bufSize;
          memcpy(dest, buf, bufSize);
        }
      }

      const UInt32 numRounds = (UInt32)1 << NumCyclesPower;
      UInt32 r = 0;
      do
      {
        Byte *dest = buf + bufSize - 8;
        UInt32 i = r;
        r += numUnroll;
        do
        {
          SetUi32(dest, i); i++; dest += bufSize;
          // SetUi32(dest, i); i++; dest += bufSize;
        }
        while (i < r);
        Sha256_Update((CSha256 *)(void *)(Byte *)sha, buf, unrollSize);
      }
      while (r < numRounds);
    }
    /*
    UInt64 numRounds = (UInt64)1 << NumCyclesPower;

    do
    {
      Sha256_Update((CSha256 *)(Byte *)sha, buf, bufSize);
      for (unsigned i = 0; i < 8; i++)
        if (++(ctr[i]) != 0)
          break;
    }
    while (--numRounds != 0);
    */

    Sha256_Final((CSha256 *)(void *)(Byte *)sha, Key);
    memset(sha, 0, sha.Size());
  }
}

bool CKeyInfoCache::GetKey(CKeyInfo &key)
{
  FOR_VECTOR (i, Keys)
  {
    const CKeyInfo &cached = Keys[i];
    if (key.IsEqualTo(cached))
    {
      for (unsigned j = 0; j < kKeySize; j++)
        key.Key[j] = cached.Key[j];
      if (i != 0)
        Keys.MoveToFront(i);
      return true;
    }
  }
  return false;
}

void CKeyInfoCache::FindAndAdd(const CKeyInfo &key)
{
  FOR_VECTOR (i, Keys)
  {
    const CKeyInfo &cached = Keys[i];
    if (key.IsEqualTo(cached))
    {
      if (i != 0)
        Keys.MoveToFront(i);
      return;
    }
  }
  Add(key);
}

void CKeyInfoCache::Add(const CKeyInfo &key)
{
  if (Keys.Size() >= Size)
    Keys.DeleteBack();
  Keys.Insert(0, key);
}

static CKeyInfoCache g_GlobalKeyCache(32);

#ifndef _7ZIP_ST
  static NWindows::NSynchronization::CCriticalSection g_GlobalKeyCacheCriticalSection;
  #define MT_LOCK NWindows::NSynchronization::CCriticalSectionLock lock(g_GlobalKeyCacheCriticalSection);
#else
  #define MT_LOCK
#endif

CBase::CBase():
  _cachedKeys(16),
  _ivSize(0)
{
  for (unsigned i = 0; i < sizeof(_iv); i++)
    _iv[i] = 0;
}

void CBase::PrepareKey()
{
  // BCJ2 threads use same password. So we use long lock.
  MT_LOCK
  
  bool finded = false;
  if (!_cachedKeys.GetKey(_key))
  {
    finded = g_GlobalKeyCache.GetKey(_key);
    if (!finded)
      _key.CalcKey();
    _cachedKeys.Add(_key);
  }
  if (!finded)
    g_GlobalKeyCache.FindAndAdd(_key);
}

#ifndef EXTRACT_ONLY

/*
STDMETHODIMP CEncoder::ResetSalt()
{
  _key.SaltSize = 4;
  g_RandomGenerator.Generate(_key.Salt, _key.SaltSize);
  return S_OK;
}
*/

STDMETHODIMP CEncoder::ResetInitVector()
{
  for (unsigned i = 0; i < sizeof(_iv); i++)
    _iv[i] = 0;
  _ivSize = 16;
  MY_RAND_GEN(_iv, _ivSize);
  return S_OK;
}

STDMETHODIMP CEncoder::WriteCoderProperties(ISequentialOutStream *outStream)
{
  Byte props[2 + sizeof(_key.Salt) + sizeof(_iv)];
  unsigned propsSize = 1;

  props[0] = (Byte)(_key.NumCyclesPower
      | (_key.SaltSize == 0 ? 0 : (1 << 7))
      | (_ivSize       == 0 ? 0 : (1 << 6)));

  if (_key.SaltSize != 0 || _ivSize != 0)
  {
    props[1] = (Byte)(
        ((_key.SaltSize == 0 ? 0 : _key.SaltSize - 1) << 4)
        | (_ivSize      == 0 ? 0 : _ivSize - 1));
    memcpy(props + 2, _key.Salt, _key.SaltSize);
    propsSize = 2 + _key.SaltSize;
    memcpy(props + propsSize, _iv, _ivSize);
    propsSize += _ivSize;
  }

  return WriteStream(outStream, props, propsSize);
}

CEncoder::CEncoder()
{
  // _key.SaltSize = 4; g_RandomGenerator.Generate(_key.Salt, _key.SaltSize);
  // _key.NumCyclesPower = 0x3F;
  _key.NumCyclesPower = 19;
  _aesFilter = new CAesCbcEncoder(kKeySize);
}

#endif

CDecoder::CDecoder()
{
  _aesFilter = new CAesCbcDecoder(kKeySize);
}

STDMETHODIMP CDecoder::SetDecoderProperties2(const Byte *data, UInt32 size)
{
  _key.ClearProps();
 
  _ivSize = 0;
  unsigned i;
  for (i = 0; i < sizeof(_iv); i++)
    _iv[i] = 0;
  
  if (size == 0)
    return S_OK;
  
  Byte b0 = data[0];

  _key.NumCyclesPower = b0 & 0x3F;
  if ((b0 & 0xC0) == 0)
    return size == 1 ? S_OK : E_INVALIDARG;

  if (size <= 1)
    return E_INVALIDARG;

  Byte b1 = data[1];

  unsigned saltSize = ((b0 >> 7) & 1) + (b1 >> 4);
  unsigned ivSize   = ((b0 >> 6) & 1) + (b1 & 0x0F);
  
  if (size != 2 + saltSize + ivSize)
    return E_INVALIDARG;
  _key.SaltSize = saltSize;
  data += 2;
  for (i = 0; i < saltSize; i++)
    _key.Salt[i] = *data++;
  for (i = 0; i < ivSize; i++)
    _iv[i] = *data++;
  return (_key.NumCyclesPower <= k_NumCyclesPower_Supported_MAX
      || _key.NumCyclesPower == 0x3F) ? S_OK : E_NOTIMPL;
}


STDMETHODIMP CBaseCoder::CryptoSetPassword(const Byte *data, UInt32 size)
{
  COM_TRY_BEGIN
  
  _key.Password.Wipe();
  _key.Password.CopyFrom(data, (size_t)size);
  return S_OK;
  
  COM_TRY_END
}

STDMETHODIMP CBaseCoder::Init()
{
  COM_TRY_BEGIN
  
  PrepareKey();
  CMyComPtr<ICryptoProperties> cp;
  RINOK(_aesFilter.QueryInterface(IID_ICryptoProperties, &cp));
  if (!cp)
    return E_FAIL;
  RINOK(cp->SetKey(_key.Key, kKeySize));
  RINOK(cp->SetInitVector(_iv, sizeof(_iv)));
  return _aesFilter->Init();
  
  COM_TRY_END
}

STDMETHODIMP_(UInt32) CBaseCoder::Filter(Byte *data, UInt32 size)
{
  return _aesFilter->Filter(data, size);
}

}}
