1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "components/sync/engine_impl/bookmark_update_preprocessing.h"
6 
7 #include <array>
8 
9 #include "base/containers/span.h"
10 #include "base/guid.h"
11 #include "base/hash/sha1.h"
12 #include "base/logging.h"
13 #include "base/metrics/histogram_functions.h"
14 #include "base/metrics/histogram_macros.h"
15 #include "base/strings/strcat.h"
16 #include "base/strings/string_util.h"
17 #include "base/strings/stringprintf.h"
18 #include "components/sync/base/hash_util.h"
19 #include "components/sync/base/unique_position.h"
20 #include "components/sync/model/entity_data.h"
21 #include "components/sync/protocol/sync.pb.h"
22 
23 namespace syncer {
24 
25 namespace {
26 
27 // Used in metric "Sync.BookmarkGUIDSource2". These values are persisted to
28 // logs. Entries should not be renumbered and numeric values should never be
29 // reused.
30 enum class BookmarkGuidSource {
31   // GUID came from specifics.
32   kSpecifics = 0,
33   // GUID came from originator_client_item_id and is valid.
34   kValidOCII = 1,
35   // GUID not found in the specifics and originator_client_item_id is invalid,
36   // so field left empty (currently unused).
37   kDeprecatedLeftEmpty = 2,
38   // GUID not found in the specifics and originator_client_item_id is invalid,
39   // so the GUID is inferred from combining originator_client_item_id and
40   // originator_cache_guid.
41   kInferred = 3,
42 
43   kMaxValue = kInferred,
44 };
45 
LogGuidSource(BookmarkGuidSource source)46 inline void LogGuidSource(BookmarkGuidSource source) {
47   base::UmaHistogramEnumeration("Sync.BookmarkGUIDSource2", source);
48 }
49 
ComputeGuidFromBytes(base::span<const uint8_t> bytes)50 std::string ComputeGuidFromBytes(base::span<const uint8_t> bytes) {
51   DCHECK_GE(bytes.size(), 16U);
52 
53   // This implementation is based on the equivalent logic in base/guid.cc.
54 
55   // Set the GUID to version 4 as described in RFC 4122, section 4.4.
56   // The format of GUID version 4 must be xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx,
57   // where y is one of [8, 9, A, B].
58 
59   // Clear the version bits and set the version to 4:
60   const uint8_t byte6 = (bytes[6] & 0x0fU) | 0xf0U;
61 
62   // Set the two most significant bits (bits 6 and 7) of the
63   // clock_seq_hi_and_reserved to zero and one, respectively:
64   const uint8_t byte8 = (bytes[8] & 0x3fU) | 0x80U;
65 
66   return base::StringPrintf(
67       "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
68       bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], byte6,
69       bytes[7], byte8, bytes[9], bytes[10], bytes[11], bytes[12], bytes[13],
70       bytes[14], bytes[15]);
71 }
72 
73 // Bookmarks created before 2015 (https://codereview.chromium.org/1136953013)
74 // have an originator client item ID that is NOT a GUID. Hence, an alternative
75 // method must be used to infer a GUID deterministically from a combination of
76 // sync fields that is known to be a) immutable and b) unique per synced
77 // bookmark.
InferGuidForLegacyBookmark(const std::string & originator_cache_guid,const std::string & originator_client_item_id)78 std::string InferGuidForLegacyBookmark(
79     const std::string& originator_cache_guid,
80     const std::string& originator_client_item_id) {
81   DCHECK(!base::IsValidGUID(originator_client_item_id));
82 
83   const std::string unique_tag =
84       base::StrCat({originator_cache_guid, originator_client_item_id});
85   const base::SHA1Digest hash =
86       base::SHA1HashSpan(base::as_bytes(base::make_span(unique_tag)));
87 
88   static_assert(base::kSHA1Length >= 16, "16 bytes needed to infer GUID");
89 
90   const std::string guid = ComputeGuidFromBytes(base::make_span(hash));
91   DCHECK(base::IsValidGUIDOutputString(guid));
92   return guid;
93 }
94 
95 }  // namespace
96 
AdaptUniquePositionForBookmark(const sync_pb::SyncEntity & update_entity,EntityData * data)97 void AdaptUniquePositionForBookmark(const sync_pb::SyncEntity& update_entity,
98                                     EntityData* data) {
99   DCHECK(data);
100 
101   // Tombstones don't need positioning information.
102   if (update_entity.deleted()) {
103     return;
104   }
105 
106   // Permanent folders don't need positioning information.
107   if (update_entity.folder() &&
108       !update_entity.server_defined_unique_tag().empty()) {
109     return;
110   }
111 
112   if (update_entity.has_unique_position()) {
113     data->unique_position = update_entity.unique_position();
114   } else if (update_entity.has_position_in_parent() ||
115              update_entity.has_insert_after_item_id()) {
116     bool missing_originator_fields = false;
117     if (!update_entity.has_originator_cache_guid() ||
118         !update_entity.has_originator_client_item_id()) {
119       DLOG(ERROR) << "Update is missing requirements for bookmark position.";
120       missing_originator_fields = true;
121     }
122 
123     std::string suffix = missing_originator_fields
124                              ? UniquePosition::RandomSuffix()
125                              : GenerateSyncableBookmarkHash(
126                                    update_entity.originator_cache_guid(),
127                                    update_entity.originator_client_item_id());
128 
129     if (update_entity.has_position_in_parent()) {
130       data->unique_position =
131           UniquePosition::FromInt64(update_entity.position_in_parent(), suffix)
132               .ToProto();
133     } else {
134       // If update_entity has insert_after_item_id, use 0 index.
135       DCHECK(update_entity.has_insert_after_item_id());
136       data->unique_position = UniquePosition::FromInt64(0, suffix).ToProto();
137     }
138   } else {
139     DLOG(ERROR) << "Missing required position information in update: "
140                 << update_entity.id_string();
141   }
142 }
143 
AdaptTitleForBookmark(const sync_pb::SyncEntity & update_entity,sync_pb::EntitySpecifics * specifics,bool specifics_were_encrypted)144 void AdaptTitleForBookmark(const sync_pb::SyncEntity& update_entity,
145                            sync_pb::EntitySpecifics* specifics,
146                            bool specifics_were_encrypted) {
147   DCHECK(specifics);
148   if (specifics_were_encrypted || update_entity.deleted()) {
149     // If encrypted, the name field is never populated (unencrypted) for privacy
150     // reasons. Encryption was also introduced after moving the name out of
151     // SyncEntity so this hack is not needed at all.
152     return;
153   }
154   // Legacy clients populate the name field in the SyncEntity instead of the
155   // title field in the BookmarkSpecifics.
156   if (!specifics->bookmark().has_legacy_canonicalized_title() &&
157       !update_entity.name().empty()) {
158     specifics->mutable_bookmark()->set_legacy_canonicalized_title(
159         update_entity.name());
160   }
161 }
162 
AdaptGuidForBookmark(const sync_pb::SyncEntity & update_entity,sync_pb::EntitySpecifics * specifics)163 bool AdaptGuidForBookmark(const sync_pb::SyncEntity& update_entity,
164                           sync_pb::EntitySpecifics* specifics) {
165   DCHECK(specifics);
166   // Tombstones and permanent entities don't have a GUID.
167   if (update_entity.deleted() ||
168       !update_entity.server_defined_unique_tag().empty()) {
169     return false;
170   }
171   // Legacy clients don't populate the guid field in the BookmarkSpecifics, so
172   // we use the originator_client_item_id instead, if it is a valid GUID.
173   // Otherwise, we leave the field empty.
174   if (specifics->bookmark().has_guid()) {
175     LogGuidSource(BookmarkGuidSource::kSpecifics);
176     return false;
177   }
178   if (base::IsValidGUID(update_entity.originator_client_item_id())) {
179     // Bookmarks created around 2016, between [M44..M52) use an uppercase GUID
180     // as originator client item ID, so it needs to be lowercased to adhere to
181     // the invariant that GUIDs in specifics are canonicalized.
182     specifics->mutable_bookmark()->set_guid(
183         base::ToLowerASCII(update_entity.originator_client_item_id()));
184     DCHECK(base::IsValidGUIDOutputString(specifics->bookmark().guid()));
185     LogGuidSource(BookmarkGuidSource::kValidOCII);
186   } else {
187     specifics->mutable_bookmark()->set_guid(
188         InferGuidForLegacyBookmark(update_entity.originator_cache_guid(),
189                                    update_entity.originator_client_item_id()));
190     DCHECK(base::IsValidGUIDOutputString(specifics->bookmark().guid()));
191     LogGuidSource(BookmarkGuidSource::kInferred);
192   }
193   return true;
194 }
195 
InferGuidForLegacyBookmarkForTesting(const std::string & originator_cache_guid,const std::string & originator_client_item_id)196 std::string InferGuidForLegacyBookmarkForTesting(
197     const std::string& originator_cache_guid,
198     const std::string& originator_client_item_id) {
199   return InferGuidForLegacyBookmark(originator_cache_guid,
200                                     originator_client_item_id);
201 }
202 
203 }  // namespace syncer
204