// 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