// Copyright 2012 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 "sync/syncable/nigori_util.h"
#include <queue>
#include <string>
#include <vector>
#include "base/json/json_writer.h"
#include "sync/syncable/directory.h"
#include "sync/syncable/entry.h"
#include "sync/syncable/nigori_handler.h"
#include "sync/syncable/mutable_entry.h"
#include "sync/syncable/syncable_util.h"
#include "sync/syncable/syncable_write_transaction.h"
#include "sync/util/cryptographer.h"
namespace syncer {
namespace syncable {
bool ProcessUnsyncedChangesForEncryption(
WriteTransaction* const trans) {
NigoriHandler* nigori_handler = trans->directory()->GetNigoriHandler();
ModelTypeSet encrypted_types = nigori_handler->GetEncryptedTypes(trans);
Cryptographer* cryptographer = trans->directory()->GetCryptographer(trans);
DCHECK(cryptographer->is_ready());
// 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).
// Note: we do not attempt to re-encrypt data with a new key here as key
// changes in this code path are likely due to consistency issues (we have
// to be updated to a key we already have, e.g. an old key).
std::vector<int64> handles;
GetUnsyncedEntries(trans, &handles);
for (size_t i = 0; i < handles.size(); ++i) {
MutableEntry entry(trans, GET_BY_HANDLE, handles[i]);
const sync_pb::EntitySpecifics& specifics = entry.GetSpecifics();
// Ignore types that don't need encryption or entries that are already
// encrypted.
if (!SpecificsNeedsEncryption(encrypted_types, specifics))
continue;
if (!UpdateEntryWithEncryption(trans, specifics, &entry))
return false;
}
return true;
}
bool VerifyUnsyncedChangesAreEncrypted(
BaseTransaction* const trans,
ModelTypeSet encrypted_types) {
std::vector<int64> handles;
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;
}
if (EntryNeedsEncryption(encrypted_types, entry))
return false;
}
return true;
}
bool EntryNeedsEncryption(ModelTypeSet encrypted_types,
const Entry& entry) {
if (!entry.GetUniqueServerTag().empty())
return false; // We don't encrypt unique server nodes.
ModelType type = entry.GetModelType();
if (type == PASSWORDS || IsControlType(type))
return false;
// Checking NON_UNIQUE_NAME is not necessary for the correctness of encrypting
// the data, nor for determining if data is encrypted. We simply ensure it has
// been overwritten to avoid any possible leaks of sensitive data.
return SpecificsNeedsEncryption(encrypted_types, entry.GetSpecifics()) ||
(encrypted_types.Has(type) &&
entry.GetNonUniqueName() != kEncryptedString);
}
bool SpecificsNeedsEncryption(ModelTypeSet encrypted_types,
const sync_pb::EntitySpecifics& specifics) {
const ModelType type = GetModelTypeFromSpecifics(specifics);
if (type == PASSWORDS || IsControlType(type))
return false; // These types have their own encryption schemes.
if (!encrypted_types.Has(type))
return false; // This type does not require encryption
return !specifics.has_encrypted();
}
// Mainly for testing.
bool VerifyDataTypeEncryptionForTest(
BaseTransaction* const trans,
ModelType type,
bool is_encrypted) {
Cryptographer* cryptographer = trans->directory()->GetCryptographer(trans);
if (type == PASSWORDS || IsControlType(type)) {
NOTREACHED();
return true;
}
Entry type_root(trans, GET_TYPE_ROOT, type);
if (!type_root.good()) {
NOTREACHED();
return false;
}
std::queue<Id> to_visit;
Id id_string = type_root.GetFirstChildId();
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.GetIsDir()) {
Id child_id_string = child.GetFirstChildId();
// Traverse the children.
to_visit.push(child_id_string);
}
const sync_pb::EntitySpecifics& specifics = child.GetSpecifics();
DCHECK_EQ(type, child.GetModelType());
DCHECK_EQ(type, GetModelTypeFromSpecifics(specifics));
// We don't encrypt the server's permanent items.
if (child.GetUniqueServerTag().empty()) {
if (specifics.has_encrypted() != is_encrypted)
return false;
if (specifics.has_encrypted()) {
if (child.GetNonUniqueName() != kEncryptedString)
return false;
if (!cryptographer->CanDecryptUsingDefaultKey(specifics.encrypted()))
return false;
}
}
// Push the successor.
to_visit.push(child.GetSuccessorId());
}
return true;
}
bool UpdateEntryWithEncryption(
BaseTransaction* const trans,
const sync_pb::EntitySpecifics& new_specifics,
syncable::MutableEntry* entry) {
NigoriHandler* nigori_handler = trans->directory()->GetNigoriHandler();
Cryptographer* cryptographer = trans->directory()->GetCryptographer(trans);
ModelType type = GetModelTypeFromSpecifics(new_specifics);
DCHECK_GE(type, FIRST_REAL_MODEL_TYPE);
const sync_pb::EntitySpecifics& old_specifics = entry->GetSpecifics();
const ModelTypeSet encrypted_types =
nigori_handler?
nigori_handler->GetEncryptedTypes(trans) : ModelTypeSet();
// It's possible the nigori lost the set of encrypted types. If the current
// specifics are already encrypted, we want to ensure we continue encrypting.
bool was_encrypted = old_specifics.has_encrypted();
sync_pb::EntitySpecifics generated_specifics;
if (new_specifics.has_encrypted()) {
NOTREACHED() << "New specifics already has an encrypted blob.";
return false;
}
if ((!SpecificsNeedsEncryption(encrypted_types, new_specifics) &&
!was_encrypted) ||
!cryptographer || !cryptographer->is_initialized()) {
// No encryption required or we are unable to encrypt.
generated_specifics.CopyFrom(new_specifics);
} else {
// Encrypt new_specifics into generated_specifics.
if (VLOG_IS_ON(2)) {
scoped_ptr<base::DictionaryValue> value(entry->ToValue(NULL));
std::string info;
base::JSONWriter::WriteWithOptions(value.get(),
base::JSONWriter::OPTIONS_PRETTY_PRINT,
&info);
DVLOG(2) << "Encrypting specifics of type "
<< ModelTypeToString(type)
<< " with content: "
<< info;
}
// Only copy over the old specifics if it is of the right type and already
// encrypted. The first time we encrypt a node we start from scratch, hence
// removing all the unencrypted data, but from then on we only want to
// update the node if the data changes or the encryption key changes.
if (GetModelTypeFromSpecifics(old_specifics) == type &&
was_encrypted) {
generated_specifics.CopyFrom(old_specifics);
} else {
AddDefaultFieldValue(type, &generated_specifics);
}
// Does not change anything if underlying encrypted blob was already up
// to date and encrypted with the default key.
if (!cryptographer->Encrypt(new_specifics,
generated_specifics.mutable_encrypted())) {
NOTREACHED() << "Could not encrypt data for node of type "
<< ModelTypeToString(type);
return false;
}
}
// It's possible this entry was encrypted but didn't properly overwrite the
// non_unique_name (see crbug.com/96314).
bool encrypted_without_overwriting_name = (was_encrypted &&
entry->GetNonUniqueName() != kEncryptedString);
// If we're encrypted but the name wasn't overwritten properly we still want
// to rewrite the entry, irrespective of whether the specifics match.
if (!encrypted_without_overwriting_name &&
old_specifics.SerializeAsString() ==
generated_specifics.SerializeAsString()) {
DVLOG(2) << "Specifics of type " << ModelTypeToString(type)
<< " already match, dropping change.";
return true;
}
if (generated_specifics.has_encrypted()) {
// Overwrite the possibly sensitive non-specifics data.
entry->PutNonUniqueName(kEncryptedString);
// For bookmarks we actually put bogus data into the unencrypted specifics,
// else the server will try to do it for us.
if (type == BOOKMARKS) {
sync_pb::BookmarkSpecifics* bookmark_specifics =
generated_specifics.mutable_bookmark();
if (!entry->GetIsDir())
bookmark_specifics->set_url(kEncryptedString);
bookmark_specifics->set_title(kEncryptedString);
}
}
entry->PutSpecifics(generated_specifics);
DVLOG(1) << "Overwriting specifics of type "
<< ModelTypeToString(type)
<< " and marking for syncing.";
syncable::MarkForSyncing(entry);
return true;
}
void UpdateNigoriFromEncryptedTypes(ModelTypeSet encrypted_types,
bool encrypt_everything,
sync_pb::NigoriSpecifics* nigori) {
nigori->set_encrypt_everything(encrypt_everything);
COMPILE_ASSERT(32 == MODEL_TYPE_COUNT, UpdateEncryptedTypes);
nigori->set_encrypt_bookmarks(
encrypted_types.Has(BOOKMARKS));
nigori->set_encrypt_preferences(
encrypted_types.Has(PREFERENCES));
nigori->set_encrypt_autofill_profile(
encrypted_types.Has(AUTOFILL_PROFILE));
nigori->set_encrypt_autofill(encrypted_types.Has(AUTOFILL));
nigori->set_encrypt_themes(encrypted_types.Has(THEMES));
nigori->set_encrypt_typed_urls(
encrypted_types.Has(TYPED_URLS));
nigori->set_encrypt_extension_settings(
encrypted_types.Has(EXTENSION_SETTINGS));
nigori->set_encrypt_extensions(
encrypted_types.Has(EXTENSIONS));
nigori->set_encrypt_search_engines(
encrypted_types.Has(SEARCH_ENGINES));
nigori->set_encrypt_sessions(encrypted_types.Has(SESSIONS));
nigori->set_encrypt_app_settings(
encrypted_types.Has(APP_SETTINGS));
nigori->set_encrypt_apps(encrypted_types.Has(APPS));
nigori->set_encrypt_app_notifications(
encrypted_types.Has(APP_NOTIFICATIONS));
nigori->set_encrypt_dictionary(encrypted_types.Has(DICTIONARY));
nigori->set_encrypt_favicon_images(encrypted_types.Has(FAVICON_IMAGES));
nigori->set_encrypt_favicon_tracking(encrypted_types.Has(FAVICON_TRACKING));
nigori->set_encrypt_articles(encrypted_types.Has(ARTICLES));
nigori->set_encrypt_app_list(encrypted_types.Has(APP_LIST));
}
ModelTypeSet GetEncryptedTypesFromNigori(
const sync_pb::NigoriSpecifics& nigori) {
if (nigori.encrypt_everything())
return ModelTypeSet::All();
ModelTypeSet encrypted_types;
COMPILE_ASSERT(32 == MODEL_TYPE_COUNT, UpdateEncryptedTypes);
if (nigori.encrypt_bookmarks())
encrypted_types.Put(BOOKMARKS);
if (nigori.encrypt_preferences())
encrypted_types.Put(PREFERENCES);
if (nigori.encrypt_autofill_profile())
encrypted_types.Put(AUTOFILL_PROFILE);
if (nigori.encrypt_autofill())
encrypted_types.Put(AUTOFILL);
if (nigori.encrypt_themes())
encrypted_types.Put(THEMES);
if (nigori.encrypt_typed_urls())
encrypted_types.Put(TYPED_URLS);
if (nigori.encrypt_extension_settings())
encrypted_types.Put(EXTENSION_SETTINGS);
if (nigori.encrypt_extensions())
encrypted_types.Put(EXTENSIONS);
if (nigori.encrypt_search_engines())
encrypted_types.Put(SEARCH_ENGINES);
if (nigori.encrypt_sessions())
encrypted_types.Put(SESSIONS);
if (nigori.encrypt_app_settings())
encrypted_types.Put(APP_SETTINGS);
if (nigori.encrypt_apps())
encrypted_types.Put(APPS);
if (nigori.encrypt_app_notifications())
encrypted_types.Put(APP_NOTIFICATIONS);
if (nigori.encrypt_dictionary())
encrypted_types.Put(DICTIONARY);
if (nigori.encrypt_favicon_images())
encrypted_types.Put(FAVICON_IMAGES);
if (nigori.encrypt_favicon_tracking())
encrypted_types.Put(FAVICON_TRACKING);
if (nigori.encrypt_articles())
encrypted_types.Put(ARTICLES);
if (nigori.encrypt_app_list())
encrypted_types.Put(APP_LIST);
return encrypted_types;
}
} // namespace syncable
} // namespace syncer