Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(171)

Side by Side Diff: components/open_from_clipboard/clipboard_recent_content_ios.mm

Issue 1288733002: Add hash to OFC pasteboard change detection (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: feedback Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #import "components/open_from_clipboard/clipboard_recent_content_ios.h" 5 #import "components/open_from_clipboard/clipboard_recent_content_ios.h"
6 6
7 #import <CommonCrypto/CommonDigest.h>
7 #import <UIKit/UIKit.h> 8 #import <UIKit/UIKit.h>
8 9
9 #import "base/ios/ios_util.h" 10 #import "base/ios/ios_util.h"
10 #include "base/logging.h" 11 #include "base/logging.h"
11 #include "base/macros.h" 12 #include "base/macros.h"
12 #include "base/metrics/user_metrics.h" 13 #include "base/metrics/user_metrics.h"
13 #include "base/strings/sys_string_conversions.h" 14 #include "base/strings/sys_string_conversions.h"
14 #include "base/sys_info.h" 15 #include "base/sys_info.h"
15 #include "url/gurl.h" 16 #include "url/gurl.h"
16 #include "url/url_constants.h" 17 #include "url/url_constants.h"
(...skipping 24 matching lines...) Expand all
41 self = [super init]; 42 self = [super init];
42 if (self) { 43 if (self) {
43 _delegate = delegate; 44 _delegate = delegate;
44 [[NSNotificationCenter defaultCenter] 45 [[NSNotificationCenter defaultCenter]
45 addObserver:self 46 addObserver:self
46 selector:@selector(pasteboardChangedNotification:) 47 selector:@selector(pasteboardChangedNotification:)
47 name:UIPasteboardChangedNotification 48 name:UIPasteboardChangedNotification
48 object:[UIPasteboard generalPasteboard]]; 49 object:[UIPasteboard generalPasteboard]];
49 [[NSNotificationCenter defaultCenter] 50 [[NSNotificationCenter defaultCenter]
50 addObserver:self 51 addObserver:self
51 selector:@selector(pasteboardChangedNotification:) 52 selector:@selector(didBecomeActive:)
52 name:UIApplicationDidBecomeActiveNotification 53 name:UIApplicationDidBecomeActiveNotification
53 object:nil]; 54 object:nil];
54 } 55 }
55 return self; 56 return self;
56 } 57 }
57 58
58 - (void)dealloc { 59 - (void)dealloc {
59 [[NSNotificationCenter defaultCenter] removeObserver:self]; 60 [[NSNotificationCenter defaultCenter] removeObserver:self];
60 [super dealloc]; 61 [super dealloc];
61 } 62 }
62 63
63 - (void)pasteboardChangedNotification:(NSNotification*)notification { 64 - (void)pasteboardChangedNotification:(NSNotification*)notification {
64 if (_delegate) { 65 if (_delegate) {
65 _delegate->PasteboardChanged(); 66 _delegate->PasteboardChanged();
66 } 67 }
67 } 68 }
68 69
70 - (void)didBecomeActive:(NSNotification*)notification {
71 if (_delegate) {
72 _delegate->LoadFromUserDefaults();
73 base::TimeDelta uptime =
74 base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime());
75 if (_delegate->HasPasteboardChanged(uptime)) {
76 _delegate->PasteboardChanged();
77 }
78 }
79 }
80
69 - (void)disconnect { 81 - (void)disconnect {
70 _delegate = nullptr; 82 _delegate = nullptr;
71 } 83 }
72 84
73 @end 85 @end
74 86
75 namespace { 87 namespace {
76 // Key used to store the pasteboard's current change count. If when resuming 88 // Key used to store the pasteboard's current change count. If when resuming
77 // chrome the pasteboard's change count is different from the stored one, then 89 // chrome the pasteboard's change count is different from the stored one, then
78 // it means that the pasteboard's content has changed. 90 // it means that the pasteboard's content has changed.
79 NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount"; 91 NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount";
80 // Key used to store the last date at which it was detected that the pasteboard 92 // Key used to store the last date at which it was detected that the pasteboard
81 // changed. It is used to evaluate the age of the pasteboard's content. 93 // changed. It is used to evaluate the age of the pasteboard's content.
82 NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate"; 94 NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate";
83 // Key used to store the 95 // Key used to store the hash of the content of the pasteboard. Whenever the
84 NSString* kSuppressedPasteboardEntryCountKey = @"PasteboardSupressedEntryCount"; 96 // hash changed, the pasteboard content is considered to have changed.
97 NSString* kPasteboardEntryMD5Key = @"PasteboardEntryMD5";
98 // Key used to store the date of the latest pasteboard entry displayed in the
99 // omnibox. This is used to report metrics on pasteboard change.
100 NSString* kLastDisplayedPasteboardEntryKey = @"LastDisplayedPasteboardEntry";
85 base::TimeDelta kMaximumAgeOfClipboard = base::TimeDelta::FromHours(3); 101 base::TimeDelta kMaximumAgeOfClipboard = base::TimeDelta::FromHours(3);
86 // Schemes accepted by the ClipboardRecentContentIOS. 102 // Schemes accepted by the ClipboardRecentContentIOS.
87 const char* kAuthorizedSchemes[] = { 103 const char* kAuthorizedSchemes[] = {
88 url::kHttpScheme, 104 url::kHttpScheme,
89 url::kHttpsScheme, 105 url::kHttpsScheme,
90 url::kDataScheme, 106 url::kDataScheme,
91 url::kAboutScheme, 107 url::kAboutScheme,
92 }; 108 };
109
110 // Compute a hash consisting of the first 4 bytes of the MD5 hash of |string|.
111 // This value is used to detect pasteboard content change. Keeping only 4 bytes
112 // is a privacy requirement to introduce collision and allow deniability of
113 // having copied a given string.
114 NSData* WeakMD5FromNSString(NSString* string) {
115 unsigned char hash[CC_MD5_DIGEST_LENGTH];
116 const char* c_string = [string UTF8String];
117 CC_MD5(c_string, strlen(c_string), hash);
118 NSData* data = [NSData dataWithBytes:hash length:4];
119 return data;
120 }
121
93 } // namespace 122 } // namespace
94 123
95 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) const { 124 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) const {
96 DCHECK(url); 125 DCHECK(url);
97 if (GetClipboardContentAge() > kMaximumAgeOfClipboard || 126 if (GetClipboardContentAge() > kMaximumAgeOfClipboard) {
98 [UIPasteboard generalPasteboard].changeCount ==
99 suppressedPasteboardEntryCount_) {
100 return false; 127 return false;
101 } 128 }
102 129
103 if (urlFromPasteboardCache_.is_valid()) { 130 if (url_from_pasteboard_cache_.is_valid()) {
104 *url = urlFromPasteboardCache_; 131 *url = url_from_pasteboard_cache_;
105 return true; 132 return true;
106 } 133 }
107 return false; 134 return false;
108 } 135 }
109 136
110 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { 137 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const {
111 return base::TimeDelta::FromSeconds( 138 return base::TimeDelta::FromSeconds(
112 static_cast<int64>(-[lastPasteboardChangeDate_ timeIntervalSinceNow])); 139 static_cast<int64>(-[last_pasteboard_change_date_ timeIntervalSinceNow]));
113 } 140 }
114 141
115 void ClipboardRecentContentIOS::SuppressClipboardContent() { 142 void ClipboardRecentContentIOS::SuppressClipboardContent() {
116 suppressedPasteboardEntryCount_ = 143 // User cleared the user data. The pasteboard entry must be removed from the
117 [UIPasteboard generalPasteboard].changeCount; 144 // omnibox list. Force entry expiration by setting copy date to 1970.
145 last_pasteboard_change_date_.reset(
146 [[NSDate alloc] initWithTimeIntervalSince1970:0]);
118 SaveToUserDefaults(); 147 SaveToUserDefaults();
119 } 148 }
120 149
121 void ClipboardRecentContentIOS::PasteboardChanged() { 150 void ClipboardRecentContentIOS::PasteboardChanged() {
122 urlFromPasteboardCache_ = URLFromPasteboard(); 151 url_from_pasteboard_cache_ = URLFromPasteboard();
123 if (!urlFromPasteboardCache_.is_empty()) { 152 if (!url_from_pasteboard_cache_.is_empty()) {
124 base::RecordAction( 153 base::RecordAction(
125 base::UserMetricsAction("MobileOmniboxClipboardChanged")); 154 base::UserMetricsAction("MobileOmniboxClipboardChanged"));
126 } 155 }
127 lastPasteboardChangeDate_.reset([[NSDate date] retain]); 156 last_pasteboard_change_date_.reset([[NSDate date] retain]);
128 lastPasteboardChangeCount_ = [UIPasteboard generalPasteboard].changeCount; 157 last_pasteboard_change_count_ = [UIPasteboard generalPasteboard].changeCount;
129 if (lastPasteboardChangeCount_ != suppressedPasteboardEntryCount_) { 158 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string];
130 suppressedPasteboardEntryCount_ = NSIntegerMax; 159 if (!pasteboard_string) {
160 pasteboard_string = @"";
131 } 161 }
162 NSData* MD5 = WeakMD5FromNSString(pasteboard_string);
163 last_pasteboard_entry_md5_.reset([MD5 retain]);
164 SaveToUserDefaults();
132 } 165 }
133 166
134 ClipboardRecentContentIOS::ClipboardRecentContentIOS( 167 ClipboardRecentContentIOS::ClipboardRecentContentIOS(
135 const std::string& application_scheme) 168 const std::string& application_scheme,
136 : application_scheme_(application_scheme) { 169 NSUserDefaults* group_user_defaults)
170 : application_scheme_(application_scheme),
171 shared_user_defaults_([group_user_defaults retain]) {
137 Init(base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime())); 172 Init(base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime()));
138 } 173 }
139 174
140 ClipboardRecentContentIOS::ClipboardRecentContentIOS( 175 ClipboardRecentContentIOS::ClipboardRecentContentIOS(
141 const std::string& application_scheme, 176 const std::string& application_scheme,
142 base::TimeDelta uptime) 177 base::TimeDelta uptime)
143 : application_scheme_(application_scheme) { 178 : application_scheme_(application_scheme),
179 shared_user_defaults_([[NSUserDefaults standardUserDefaults] retain]) {
144 Init(uptime); 180 Init(uptime);
145 } 181 }
146 182
183 bool ClipboardRecentContentIOS::HasPasteboardChanged(base::TimeDelta uptime) {
184 NSInteger change_count = [UIPasteboard generalPasteboard].changeCount;
185 bool change_count_changed = change_count != last_pasteboard_change_count_;
186
187 bool not_rebooted = uptime > GetClipboardContentAge();
droger 2015/08/13 11:42:21 Optional: should we early return here if (not_rebo
Olivier 2015/08/13 12:01:58 Done.
Olivier 2015/08/13 12:01:58 Done.
188
189 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string];
190 if (!pasteboard_string) {
191 pasteboard_string = @"";
192 }
193 NSData* md5 = WeakMD5FromNSString(pasteboard_string);
194 BOOL md5_changed = ![md5 isEqualToData:last_pasteboard_entry_md5_];
droger 2015/08/13 11:42:21 What happens if there is a collision? Is this a pr
droger 2015/08/13 11:51:52 Resolved by offline discussion: hash collisions ar
195
196 // If |MD5Changed|, we know for sure there has been at least one pasteboard
197 // copy since last time it was checked.
198 // If the pasteboard content is still the same but the device was not
199 // rebooted, the change count can be checked to see if it changed.
200 // Note: due to a mismatch between the actual behavior and documentation, and
201 // lack of consistency on different reboot scenarios, the change count cannot
202 // be checked after a reboot.
203 // See radar://21833556 for more information.
204 return md5_changed || (not_rebooted && change_count_changed);
205 }
206
147 void ClipboardRecentContentIOS::Init(base::TimeDelta uptime) { 207 void ClipboardRecentContentIOS::Init(base::TimeDelta uptime) {
148 lastPasteboardChangeCount_ = NSIntegerMax; 208 last_pasteboard_change_count_ = NSIntegerMax;
149 suppressedPasteboardEntryCount_ = NSIntegerMax; 209 url_from_pasteboard_cache_ = URLFromPasteboard();
150 urlFromPasteboardCache_ = URLFromPasteboard();
151 LoadFromUserDefaults(); 210 LoadFromUserDefaults();
152 211
153 // On iOS 7 (unlike on iOS 8, despite what the documentation says), the change 212 if (HasPasteboardChanged(uptime))
154 // count is reset when the device is rebooted. 213 PasteboardChanged();
155 if (uptime < GetClipboardContentAge() && 214
156 !base::ios::IsRunningOnIOS8OrLater()) { 215 // Makes sure |last_pasteboard_change_count_| was properly initialized.
157 if ([UIPasteboard generalPasteboard].changeCount == 0) { 216 DCHECK_NE(last_pasteboard_change_count_, NSIntegerMax);
158 // The user hasn't pasted anything in the clipboard since the device's 217 notification_bridge_.reset(
159 // reboot. |PasteboardChanged| isn't called because it would update
160 // |lastPasteboardChangeData_|, and record metrics.
161 lastPasteboardChangeCount_ = 0;
162 if (suppressedPasteboardEntryCount_ != NSIntegerMax) {
163 // If the last time Chrome was running the pasteboard was suppressed,
164 // and the user has not copied anything since the device launched, then
165 // supress this entry.
166 suppressedPasteboardEntryCount_ = 0;
167 }
168 SaveToUserDefaults();
169 } else {
170 // The user pasted something in the clipboard since the device's reboot.
171 PasteboardChanged();
172 }
173 } else {
174 NSInteger changeCount = [UIPasteboard generalPasteboard].changeCount;
175 if (changeCount != lastPasteboardChangeCount_) {
176 PasteboardChanged();
177 }
178 }
179 // Makes sure |lastPasteboardChangeCount_| was properly initialized.
180 DCHECK_NE(lastPasteboardChangeCount_, NSIntegerMax);
181 notificationBridge_.reset(
182 [[PasteboardNotificationListenerBridge alloc] initWithDelegate:this]); 218 [[PasteboardNotificationListenerBridge alloc] initWithDelegate:this]);
183 } 219 }
184 220
185 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() { 221 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() {
186 [notificationBridge_ disconnect]; 222 [notification_bridge_ disconnect];
187 } 223 }
188 224
189 GURL ClipboardRecentContentIOS::URLFromPasteboard() { 225 GURL ClipboardRecentContentIOS::URLFromPasteboard() {
190 const std::string clipboard = 226 const std::string clipboard =
191 base::SysNSStringToUTF8([[UIPasteboard generalPasteboard] string]); 227 base::SysNSStringToUTF8([[UIPasteboard generalPasteboard] string]);
192 GURL gurl = GURL(clipboard); 228 GURL gurl = GURL(clipboard);
193 if (gurl.is_valid()) { 229 if (gurl.is_valid()) {
194 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) { 230 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) {
195 if (gurl.SchemeIs(kAuthorizedSchemes[i])) { 231 if (gurl.SchemeIs(kAuthorizedSchemes[i])) {
196 return gurl; 232 return gurl;
197 } 233 }
198 } 234 }
199 if (!application_scheme_.empty() && 235 if (!application_scheme_.empty() &&
200 gurl.SchemeIs(application_scheme_.c_str())) { 236 gurl.SchemeIs(application_scheme_.c_str())) {
201 return gurl; 237 return gurl;
202 } 238 }
203 } 239 }
204 return GURL::EmptyGURL(); 240 return GURL::EmptyGURL();
205 } 241 }
206 242
243 void ClipboardRecentContentIOS::RecentURLDisplayed() {
244 if ([last_pasteboard_change_date_
245 isEqualToDate:last_displayed_pasteboard_entry_.get()]) {
246 return;
247 }
248 base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged"));
249 last_pasteboard_change_date_ = last_displayed_pasteboard_entry_;
250 SaveToUserDefaults();
251 }
252
207 void ClipboardRecentContentIOS::LoadFromUserDefaults() { 253 void ClipboardRecentContentIOS::LoadFromUserDefaults() {
208 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; 254 last_pasteboard_change_count_ =
255 [shared_user_defaults_ integerForKey:kPasteboardChangeCountKey];
256 last_pasteboard_change_date_.reset(
257 [[shared_user_defaults_ objectForKey:kPasteboardChangeDateKey] retain]);
258 last_pasteboard_entry_md5_.reset(
259 [[shared_user_defaults_ objectForKey:kPasteboardEntryMD5Key] retain]);
260 last_displayed_pasteboard_entry_.reset([[shared_user_defaults_
261 objectForKey:kLastDisplayedPasteboardEntryKey] retain]);
209 262
210 lastPasteboardChangeCount_ = 263 DCHECK(!last_pasteboard_change_date_ ||
211 [defaults integerForKey:kPasteboardChangeCountKey]; 264 [last_pasteboard_change_date_ isKindOfClass:[NSDate class]]);
212 lastPasteboardChangeDate_.reset(
213 [[defaults objectForKey:kPasteboardChangeDateKey] retain]);
214
215 if ([[[defaults dictionaryRepresentation] allKeys]
216 containsObject:kSuppressedPasteboardEntryCountKey]) {
217 suppressedPasteboardEntryCount_ =
218 [defaults integerForKey:kSuppressedPasteboardEntryCountKey];
219 } else {
220 suppressedPasteboardEntryCount_ = NSIntegerMax;
221 }
222
223 DCHECK(!lastPasteboardChangeDate_ ||
224 [lastPasteboardChangeDate_ isKindOfClass:[NSDate class]]);
225 } 265 }
226 266
227 void ClipboardRecentContentIOS::SaveToUserDefaults() { 267 void ClipboardRecentContentIOS::SaveToUserDefaults() {
228 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; 268 [shared_user_defaults_ setInteger:last_pasteboard_change_count_
229 [defaults setInteger:lastPasteboardChangeCount_ 269 forKey:kPasteboardChangeCountKey];
230 forKey:kPasteboardChangeCountKey]; 270 [shared_user_defaults_ setObject:last_pasteboard_change_date_
231 [defaults setObject:lastPasteboardChangeDate_ 271 forKey:kPasteboardChangeDateKey];
232 forKey:kPasteboardChangeDateKey]; 272 [shared_user_defaults_ setObject:last_pasteboard_entry_md5_
233 [defaults setInteger:suppressedPasteboardEntryCount_ 273 forKey:kPasteboardEntryMD5Key];
234 forKey:kSuppressedPasteboardEntryCountKey]; 274 [shared_user_defaults_ setObject:last_displayed_pasteboard_entry_
275 forKey:kLastDisplayedPasteboardEntryKey];
235 } 276 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698