// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/sync/syncable/nigori_util.h"

#include <queue>
#include <string>
#include <vector>

#include "chrome/browser/sync/engine/syncer_util.h"
#include "chrome/browser/sync/syncable/syncable.h"
#include "chrome/browser/sync/util/cryptographer.h"

namespace syncable {

ModelTypeSet GetEncryptedDataTypes(BaseTransaction* const trans) {
  std::string nigori_tag = ModelTypeToRootTag(syncable::NIGORI);
  Entry entry(trans, GET_BY_SERVER_TAG, nigori_tag);
  if (!entry.good()) {
    VLOG(1) << "Nigori node not found, assuming no encrypted datatypes.";
    return ModelTypeSet();
  }
  if (NIGORI != entry.GetModelType()) {
    // Can happen if we fail to apply the nigori node due to a conflict.
    VLOG(1) << "Nigori node does not have nigori extension. Assuming no"
            << " encrypted datatypes.";
    return ModelTypeSet();
  }
  const sync_pb::EntitySpecifics& specifics = entry.Get(SPECIFICS);
  return GetEncryptedDataTypesFromNigori(
      specifics.GetExtension(sync_pb::nigori));
}

ModelTypeSet GetEncryptedDataTypesFromNigori(
    const sync_pb::NigoriSpecifics& nigori) {
  // We don't check NIGORI datatype, it uses its own encryption scheme.
  ModelTypeSet encrypted_types;
  if (nigori.encrypt_bookmarks())
    encrypted_types.insert(BOOKMARKS);
  if (nigori.encrypt_preferences())
    encrypted_types.insert(PREFERENCES);
  if (nigori.encrypt_autofill_profile())
    encrypted_types.insert(AUTOFILL_PROFILE);
  if (nigori.encrypt_autofill())
    encrypted_types.insert(AUTOFILL);
  if (nigori.encrypt_themes())
    encrypted_types.insert(THEMES);
  if (nigori.encrypt_typed_urls())
    encrypted_types.insert(TYPED_URLS);
  if (nigori.encrypt_extensions())
    encrypted_types.insert(EXTENSIONS);
  if (nigori.encrypt_sessions())
    encrypted_types.insert(SESSIONS);
  if (nigori.encrypt_apps())
    encrypted_types.insert(APPS);
  return encrypted_types;
}

void FillNigoriEncryptedTypes(const ModelTypeSet& types,
    sync_pb::NigoriSpecifics* nigori) {
  DCHECK(nigori);
  nigori->set_encrypt_bookmarks(types.count(BOOKMARKS) > 0);
  nigori->set_encrypt_preferences(types.count(PREFERENCES) > 0);
  nigori->set_encrypt_autofill_profile(types.count(AUTOFILL_PROFILE) > 0);
  nigori->set_encrypt_autofill(types.count(AUTOFILL) > 0);
  nigori->set_encrypt_themes(types.count(THEMES) > 0);
  nigori->set_encrypt_typed_urls(types.count(TYPED_URLS) > 0);
  nigori->set_encrypt_extensions(types.count(EXTENSIONS) > 0);
  nigori->set_encrypt_sessions(types.count(SESSIONS) > 0);
  nigori->set_encrypt_apps(types.count(APPS) > 0);
}

bool ProcessUnsyncedChangesForEncryption(
    WriteTransaction* const trans,
    const ModelTypeSet& encrypted_types,
    browser_sync::Cryptographer* cryptographer) {
  // Get list of all datatypes with unsynced changes. It's possible that our
  // local changes need to be encrypted if encryption for that datatype was
  // just turned on (and vice versa). This should never affect passwords.
  std::vector<int64> handles;
  browser_sync::SyncerUtil::GetUnsyncedEntries(trans, &handles);
  for (size_t i = 0; i < handles.size(); ++i) {
    MutableEntry entry(trans, GET_BY_HANDLE, handles[i]);
    sync_pb::EntitySpecifics new_specifics;
    const sync_pb::EntitySpecifics& entry_specifics = entry.Get(SPECIFICS);
    ModelType type = entry.GetModelType();
    if (type == PASSWORDS)
      continue;
    if (encrypted_types.count(type) > 0 &&
        !entry_specifics.has_encrypted()) {
      // This entry now requires encryption.
      AddDefaultExtensionValue(type, &new_specifics);
      if (!cryptographer->Encrypt(
          entry_specifics,
          new_specifics.mutable_encrypted())) {
        LOG(ERROR) << "Could not encrypt data for newly encrypted type " <<
            ModelTypeToString(type);
        NOTREACHED();
        return false;
      } else {
        VLOG(1) << "Encrypted change for newly encrypted type " <<
            ModelTypeToString(type);
        entry.Put(SPECIFICS, new_specifics);
      }
    } else if (encrypted_types.count(type) == 0 &&
               entry_specifics.has_encrypted()) {
      // This entry no longer requires encryption.
      if (!cryptographer->Decrypt(entry_specifics.encrypted(),
                                  &new_specifics)) {
        LOG(ERROR) << "Could not decrypt data for newly unencrypted type " <<
            ModelTypeToString(type);
        NOTREACHED();
        return false;
      } else {
        VLOG(1) << "Decrypted change for newly unencrypted type " <<
            ModelTypeToString(type);
        entry.Put(SPECIFICS, new_specifics);
      }
    }
  }
  return true;
}

bool VerifyUnsyncedChangesAreEncrypted(
    BaseTransaction* const trans,
    const ModelTypeSet& encrypted_types) {
  std::vector<int64> handles;
  browser_sync::SyncerUtil::GetUnsyncedEntries(trans, &handles);
  for (size_t i = 0; i < handles.size(); ++i) {
    Entry entry(trans, GET_BY_HANDLE, handles[i]);
    if (!entry.good()) {
      NOTREACHED();
      return false;
    }
    const sync_pb::EntitySpecifics& entry_specifics = entry.Get(SPECIFICS);
    ModelType type = entry.GetModelType();
    if (type == PASSWORDS)
      continue;
    if (encrypted_types.count(type) > 0 &&
        !entry_specifics.has_encrypted()) {
      // This datatype requires encryption but this data is not encrypted.
      return false;
    }
  }
  return true;
}

// Mainly for testing.
bool VerifyDataTypeEncryption(BaseTransaction* const trans,
                              ModelType type,
                              bool is_encrypted) {
  if (type == PASSWORDS || type == NIGORI) {
    NOTREACHED();
    return true;
  }
  std::string type_tag = ModelTypeToRootTag(type);
  Entry type_root(trans, GET_BY_SERVER_TAG, type_tag);
  if (!type_root.good()) {
    NOTREACHED();
    return false;
  }

  std::queue<Id> to_visit;
  Id id_string =
      trans->directory()->GetFirstChildId(trans, type_root.Get(ID));
  to_visit.push(id_string);
  while (!to_visit.empty()) {
    id_string = to_visit.front();
    to_visit.pop();
    if (id_string.IsRoot())
      continue;

    Entry child(trans, GET_BY_ID, id_string);
    if (!child.good()) {
      NOTREACHED();
      return false;
    }
    if (child.Get(IS_DIR)) {
      // Traverse the children.
      to_visit.push(
          trans->directory()->GetFirstChildId(trans, child.Get(ID)));
    } else {
      const sync_pb::EntitySpecifics& specifics = child.Get(SPECIFICS);
      DCHECK_EQ(type, child.GetModelType());
      DCHECK_EQ(type, GetModelTypeFromSpecifics(specifics));
      if (specifics.has_encrypted() != is_encrypted)
        return false;
    }
    // Push the successor.
    to_visit.push(child.Get(NEXT_ID));
  }
  return true;
}

}  // namespace syncable