OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011 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 // This class isn't pretty. It's just a step better than globals, which is what | |
6 // these were previously. | |
7 | |
8 #include "chrome/browser/sync/util/user_settings.h" | |
9 | |
10 #include "build/build_config.h" | |
11 | |
12 #if defined(OS_WIN) | |
13 #include <windows.h> | |
14 #endif | |
15 | |
16 #include <limits> | |
17 #include <string> | |
18 #include <vector> | |
19 | |
20 #include "base/file_util.h" | |
21 #include "base/md5.h" | |
22 #include "base/rand_util.h" | |
23 #include "base/string_util.h" | |
24 #include "chrome/browser/sync/syncable/directory_manager.h" // For migration. | |
25 #include "chrome/browser/sync/util/data_encryption.h" | |
26 #include "chrome/browser/sync/util/sqlite_utils.h" | |
27 #include "chrome/common/random.h" | |
28 | |
29 using std::numeric_limits; | |
30 using std::string; | |
31 using std::vector; | |
32 | |
33 using syncable::DirectoryManager; | |
34 | |
35 namespace browser_sync { | |
36 | |
37 void ExecOrDie(sqlite3* dbhandle, const char *query) { | |
38 sqlite_utils::SQLStatement statement; | |
39 statement.prepare(dbhandle, query); | |
40 if (SQLITE_DONE != statement.step()) { | |
41 LOG(FATAL) << query << "\n" << sqlite3_errmsg(dbhandle); | |
42 } | |
43 } | |
44 | |
45 // Useful for encoding any sequence of bytes into a string that can be used in | |
46 // a table name. Kind of like hex encoding, except that A is zero and P is 15. | |
47 string APEncode(const string& in) { | |
48 string result; | |
49 result.reserve(in.size() * 2); | |
50 for (string::const_iterator i = in.begin(); i != in.end(); ++i) { | |
51 unsigned int c = static_cast<unsigned char>(*i); | |
52 result.push_back((c & 0x0F) + 'A'); | |
53 result.push_back(((c >> 4) & 0x0F) + 'A'); | |
54 } | |
55 return result; | |
56 } | |
57 | |
58 string APDecode(const string& in) { | |
59 string result; | |
60 result.reserve(in.size() / 2); | |
61 for (string::const_iterator i = in.begin(); i != in.end(); ++i) { | |
62 unsigned int c = *i - 'A'; | |
63 if (++i != in.end()) | |
64 c = c | (static_cast<unsigned char>(*i - 'A') << 4); | |
65 result.push_back(c); | |
66 } | |
67 return result; | |
68 } | |
69 | |
70 static const char PASSWORD_HASH[] = "password_hash2"; | |
71 static const char SALT[] = "salt2"; | |
72 | |
73 static const int kSaltSize = 20; | |
74 static const int kCurrentDBVersion = 12; | |
75 | |
76 UserSettings::ScopedDBHandle::ScopedDBHandle(UserSettings* settings) | |
77 : mutex_lock_(settings->dbhandle_mutex_), handle_(&settings->dbhandle_) { | |
78 } | |
79 | |
80 UserSettings::UserSettings() : dbhandle_(NULL) { | |
81 } | |
82 | |
83 string UserSettings::email() const { | |
84 base::AutoLock lock(mutex_); | |
85 return email_; | |
86 } | |
87 | |
88 static void MakeSigninsTable(sqlite3* const dbhandle) { | |
89 // Multiple email addresses can map to the same Google Account. This table | |
90 // keeps a map of sign-in email addresses to primary Google Account email | |
91 // addresses. | |
92 ExecOrDie(dbhandle, | |
93 "CREATE TABLE signins" | |
94 " (signin, primary_email, " | |
95 " PRIMARY KEY(signin, primary_email) ON CONFLICT REPLACE)"); | |
96 } | |
97 | |
98 void UserSettings::MigrateOldVersionsAsNeeded(sqlite3* const handle, | |
99 int current_version) { | |
100 switch (current_version) { | |
101 // Versions 1-9 are unhandled. Version numbers greater than | |
102 // kCurrentDBVersion should have already been weeded out by the caller. | |
103 default: | |
104 // When the version is too old, we just try to continue anyway. There | |
105 // should not be a released product that makes a database too old for us | |
106 // to handle. | |
107 LOG(WARNING) << "UserSettings database version " << current_version << | |
108 " is too old to handle."; | |
109 return; | |
110 case 10: | |
111 { | |
112 // Scrape the 'shares' table to find the syncable DB. 'shares' had a | |
113 // pair of string columns that mapped the username to the filename of | |
114 // the sync data sqlite3 file. Version 11 switched to a constant | |
115 // filename, so here we read the string, copy the file to the new name, | |
116 // delete the old one, and then drop the unused shares table. | |
117 sqlite_utils::SQLStatement share_query; | |
118 share_query.prepare(handle, "SELECT share_name, file_name FROM shares"); | |
119 int query_result = share_query.step(); | |
120 CHECK(SQLITE_ROW == query_result); | |
121 FilePath::StringType share_name, file_name; | |
122 #if defined(OS_POSIX) | |
123 share_name = share_query.column_string(0); | |
124 file_name = share_query.column_string(1); | |
125 #else | |
126 share_name = share_query.column_wstring(0); | |
127 file_name = share_query.column_wstring(1); | |
128 #endif | |
129 | |
130 const FilePath& src_syncdata_path = FilePath(file_name); | |
131 FilePath dst_syncdata_path(src_syncdata_path.DirName()); | |
132 file_util::AbsolutePath(&dst_syncdata_path); | |
133 dst_syncdata_path = dst_syncdata_path.Append( | |
134 DirectoryManager::GetSyncDataDatabaseFilename()); | |
135 if (!file_util::Move(src_syncdata_path, dst_syncdata_path)) { | |
136 LOG(WARNING) << "Unable to upgrade UserSettings from v10"; | |
137 return; | |
138 } | |
139 } | |
140 ExecOrDie(handle, "DROP TABLE shares"); | |
141 ExecOrDie(handle, "UPDATE db_version SET version = 11"); | |
142 // FALL THROUGH | |
143 case 11: | |
144 ExecOrDie(handle, "DROP TABLE signin_types"); | |
145 ExecOrDie(handle, "UPDATE db_version SET version = 12"); | |
146 // FALL THROUGH | |
147 case kCurrentDBVersion: | |
148 // Nothing to migrate. | |
149 return; | |
150 } | |
151 } | |
152 | |
153 static void MakeCookiesTable(sqlite3* const dbhandle) { | |
154 // This table keeps a list of auth tokens for each signed in account. There | |
155 // will be as many rows as there are auth tokens per sign in. | |
156 // The service_token column will store encrypted values. | |
157 ExecOrDie(dbhandle, | |
158 "CREATE TABLE cookies" | |
159 " (email, service_name, service_token, " | |
160 " PRIMARY KEY(email, service_name) ON CONFLICT REPLACE)"); | |
161 } | |
162 | |
163 static void MakeClientIDTable(sqlite3* const dbhandle) { | |
164 // Stores a single client ID value that can be used as the client id, if | |
165 // there's not another such ID provided on the install. | |
166 ExecOrDie(dbhandle, "CREATE TABLE client_id (id) "); | |
167 { | |
168 sqlite_utils::SQLStatement statement; | |
169 statement.prepare(dbhandle, | |
170 "INSERT INTO client_id values ( ? )"); | |
171 statement.bind_string(0, Generate128BitRandomBase64String()); | |
172 if (SQLITE_DONE != statement.step()) { | |
173 LOG(FATAL) << "INSERT INTO client_id\n" << sqlite3_errmsg(dbhandle); | |
174 } | |
175 } | |
176 } | |
177 | |
178 bool UserSettings::Init(const FilePath& settings_path) { | |
179 { // Scope the handle. | |
180 ScopedDBHandle dbhandle(this); | |
181 if (dbhandle_) | |
182 sqlite3_close(dbhandle_); | |
183 | |
184 if (SQLITE_OK != sqlite_utils::OpenSqliteDb(settings_path, &dbhandle_)) | |
185 return false; | |
186 | |
187 // In the worst case scenario, the user may hibernate his computer during | |
188 // one of our transactions. | |
189 sqlite3_busy_timeout(dbhandle_, numeric_limits<int>::max()); | |
190 ExecOrDie(dbhandle.get(), "PRAGMA fullfsync = 1"); | |
191 ExecOrDie(dbhandle.get(), "PRAGMA synchronous = 2"); | |
192 | |
193 sqlite_utils::SQLTransaction transaction(dbhandle.get()); | |
194 transaction.BeginExclusive(); | |
195 sqlite_utils::SQLStatement table_query; | |
196 table_query.prepare(dbhandle.get(), | |
197 "select count(*) from sqlite_master" | |
198 " where type = 'table' and name = 'db_version'"); | |
199 int query_result = table_query.step(); | |
200 CHECK(SQLITE_ROW == query_result); | |
201 int table_count = table_query.column_int(0); | |
202 table_query.reset(); | |
203 if (table_count > 0) { | |
204 sqlite_utils::SQLStatement version_query; | |
205 version_query.prepare(dbhandle.get(), | |
206 "SELECT version FROM db_version"); | |
207 query_result = version_query.step(); | |
208 CHECK(SQLITE_ROW == query_result); | |
209 const int version = version_query.column_int(0); | |
210 version_query.reset(); | |
211 if (version > kCurrentDBVersion) { | |
212 LOG(WARNING) << "UserSettings database is too new."; | |
213 return false; | |
214 } | |
215 | |
216 MigrateOldVersionsAsNeeded(dbhandle.get(), version); | |
217 } else { | |
218 // Create settings table. | |
219 { | |
220 sqlite_utils::SQLStatement statement; | |
221 statement.prepare(dbhandle.get(), | |
222 "CREATE TABLE settings" | |
223 " (email, key, value, " | |
224 " PRIMARY KEY(email, key) ON CONFLICT REPLACE)"); | |
225 if (SQLITE_DONE != statement.step()) { | |
226 return false; | |
227 } | |
228 } | |
229 // Create and populate version table. | |
230 { | |
231 sqlite_utils::SQLStatement statement; | |
232 statement.prepare(dbhandle.get(), | |
233 "CREATE TABLE db_version ( version )"); | |
234 if (SQLITE_DONE != statement.step()) { | |
235 return false; | |
236 } | |
237 } | |
238 { | |
239 sqlite_utils::SQLStatement statement; | |
240 statement.prepare(dbhandle.get(), | |
241 "INSERT INTO db_version values ( ? )"); | |
242 statement.bind_int(0, kCurrentDBVersion); | |
243 if (SQLITE_DONE != statement.step()) { | |
244 return false; | |
245 } | |
246 } | |
247 | |
248 MakeSigninsTable(dbhandle.get()); | |
249 MakeCookiesTable(dbhandle.get()); | |
250 MakeClientIDTable(dbhandle.get()); | |
251 } | |
252 transaction.Commit(); | |
253 } | |
254 #if defined(OS_WIN) | |
255 // Do not index this file. Scanning can occur every time we close the file, | |
256 // which causes long delays in SQLite's file locking. | |
257 const DWORD attrs = GetFileAttributes(settings_path.value().c_str()); | |
258 const BOOL attrs_set = | |
259 SetFileAttributes(settings_path.value().c_str(), | |
260 attrs | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); | |
261 #endif | |
262 return true; | |
263 } | |
264 | |
265 UserSettings::~UserSettings() { | |
266 if (dbhandle_) | |
267 sqlite3_close(dbhandle_); | |
268 } | |
269 | |
270 const int32 kInvalidHash = 0xFFFFFFFF; | |
271 | |
272 // We use 10 bits of data from the MD5 digest as the hash. | |
273 const int32 kHashMask = 0x3FF; | |
274 | |
275 int32 GetHashFromDigest(base::MD5Digest& digest) { | |
276 int32 hash = 0; | |
277 int32 mask = kHashMask; | |
278 for (size_t i = 0; i < sizeof(digest.a); ++i) { | |
279 hash = hash << 8; | |
280 hash = hash | (digest.a[i] & kHashMask); | |
281 mask = mask >> 8; | |
282 if (0 == mask) | |
283 break; | |
284 } | |
285 return hash; | |
286 } | |
287 | |
288 void UserSettings::StoreEmailForSignin(const string& signin, | |
289 const string& primary_email) { | |
290 ScopedDBHandle dbhandle(this); | |
291 sqlite_utils::SQLTransaction transaction(dbhandle.get()); | |
292 int sqlite_result = transaction.BeginExclusive(); | |
293 CHECK(SQLITE_OK == sqlite_result); | |
294 sqlite_utils::SQLStatement query; | |
295 query.prepare(dbhandle.get(), | |
296 "SELECT COUNT(*) FROM signins" | |
297 " WHERE signin = ? AND primary_email = ?"); | |
298 query.bind_string(0, signin); | |
299 query.bind_string(1, primary_email); | |
300 int query_result = query.step(); | |
301 CHECK(SQLITE_ROW == query_result); | |
302 int32 count = query.column_int(0); | |
303 query.reset(); | |
304 if (0 == count) { | |
305 // Migrate any settings the user might have from earlier versions. | |
306 { | |
307 sqlite_utils::SQLStatement statement; | |
308 statement.prepare(dbhandle.get(), | |
309 "UPDATE settings SET email = ? WHERE email = ?"); | |
310 statement.bind_string(0, signin); | |
311 statement.bind_string(1, primary_email); | |
312 if (SQLITE_DONE != statement.step()) { | |
313 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); | |
314 } | |
315 } | |
316 // Store this signin:email mapping. | |
317 { | |
318 sqlite_utils::SQLStatement statement; | |
319 statement.prepare(dbhandle.get(), | |
320 "INSERT INTO signins(signin, primary_email)" | |
321 " values ( ?, ? )"); | |
322 statement.bind_string(0, signin); | |
323 statement.bind_string(1, primary_email); | |
324 if (SQLITE_DONE != statement.step()) { | |
325 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); | |
326 } | |
327 } | |
328 } | |
329 transaction.Commit(); | |
330 } | |
331 | |
332 // string* signin is both the input and the output of this function. | |
333 bool UserSettings::GetEmailForSignin(string* signin) { | |
334 ScopedDBHandle dbhandle(this); | |
335 string result; | |
336 sqlite_utils::SQLStatement query; | |
337 query.prepare(dbhandle.get(), | |
338 "SELECT primary_email FROM signins WHERE signin = ?"); | |
339 query.bind_string(0, *signin); | |
340 int query_result = query.step(); | |
341 if (SQLITE_ROW == query_result) { | |
342 query.column_string(0, &result); | |
343 if (!result.empty()) { | |
344 swap(result, *signin); | |
345 return true; | |
346 } | |
347 } | |
348 return false; | |
349 } | |
350 | |
351 void UserSettings::StoreHashedPassword(const string& email, | |
352 const string& password) { | |
353 // Save one-way hashed password: | |
354 char binary_salt[kSaltSize]; | |
355 base::RandBytes(binary_salt, sizeof(binary_salt)); | |
356 | |
357 const string salt = APEncode(string(binary_salt, sizeof(binary_salt))); | |
358 base::MD5Context md5_context; | |
359 base::MD5Init(&md5_context); | |
360 base::MD5Update(&md5_context, salt); | |
361 base::MD5Update(&md5_context, password); | |
362 base::MD5Digest md5_digest; | |
363 base::MD5Final(&md5_digest, &md5_context); | |
364 | |
365 ScopedDBHandle dbhandle(this); | |
366 sqlite_utils::SQLTransaction transaction(dbhandle.get()); | |
367 transaction.BeginExclusive(); | |
368 { | |
369 sqlite_utils::SQLStatement statement; | |
370 statement.prepare(dbhandle.get(), | |
371 "INSERT INTO settings(email, key, value)" | |
372 " values ( ?, ?, ? )"); | |
373 statement.bind_string(0, email); | |
374 statement.bind_string(1, PASSWORD_HASH); | |
375 statement.bind_int(2, GetHashFromDigest(md5_digest)); | |
376 if (SQLITE_DONE != statement.step()) { | |
377 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); | |
378 } | |
379 } | |
380 { | |
381 sqlite_utils::SQLStatement statement; | |
382 statement.prepare(dbhandle.get(), | |
383 "INSERT INTO settings(email, key, value)" | |
384 " values ( ?, ?, ? )"); | |
385 statement.bind_string(0, email); | |
386 statement.bind_string(1, SALT); | |
387 statement.bind_string(2, salt); | |
388 if (SQLITE_DONE != statement.step()) { | |
389 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); | |
390 } | |
391 } | |
392 transaction.Commit(); | |
393 } | |
394 | |
395 bool UserSettings::VerifyAgainstStoredHash(const string& email, | |
396 const string& password) { | |
397 ScopedDBHandle dbhandle(this); | |
398 string salt_and_digest; | |
399 | |
400 sqlite_utils::SQLStatement query; | |
401 query.prepare(dbhandle.get(), | |
402 "SELECT key, value FROM settings" | |
403 " WHERE email = ? AND (key = ? OR key = ?)"); | |
404 query.bind_string(0, email); | |
405 query.bind_string(1, PASSWORD_HASH); | |
406 query.bind_string(2, SALT); | |
407 int query_result = query.step(); | |
408 string salt; | |
409 int32 hash = kInvalidHash; | |
410 while (SQLITE_ROW == query_result) { | |
411 string key(query.column_string(0)); | |
412 if (key == SALT) | |
413 salt = query.column_string(1); | |
414 else | |
415 hash = query.column_int(1); | |
416 query_result = query.step(); | |
417 } | |
418 CHECK(SQLITE_DONE == query_result); | |
419 if (salt.empty() || hash == kInvalidHash) | |
420 return false; | |
421 base::MD5Context md5_context; | |
422 base::MD5Init(&md5_context); | |
423 base::MD5Update(&md5_context, salt); | |
424 base::MD5Update(&md5_context, password); | |
425 base::MD5Digest md5_digest; | |
426 base::MD5Final(&md5_digest, &md5_context); | |
427 return hash == GetHashFromDigest(md5_digest); | |
428 } | |
429 | |
430 void UserSettings::SwitchUser(const string& username) { | |
431 { | |
432 base::AutoLock lock(mutex_); | |
433 email_ = username; | |
434 } | |
435 } | |
436 | |
437 string UserSettings::GetClientId() { | |
438 ScopedDBHandle dbhandle(this); | |
439 sqlite_utils::SQLStatement statement; | |
440 statement.prepare(dbhandle.get(), "SELECT id FROM client_id"); | |
441 int query_result = statement.step(); | |
442 string client_id; | |
443 if (query_result == SQLITE_ROW) | |
444 client_id = statement.column_string(0); | |
445 return client_id; | |
446 } | |
447 | |
448 void UserSettings::ClearAllServiceTokens() { | |
449 ScopedDBHandle dbhandle(this); | |
450 ExecOrDie(dbhandle.get(), "DELETE FROM cookies"); | |
451 } | |
452 | |
453 bool UserSettings::GetLastUser(string* username) { | |
454 ScopedDBHandle dbhandle(this); | |
455 sqlite_utils::SQLStatement query; | |
456 query.prepare(dbhandle.get(), "SELECT email FROM cookies"); | |
457 if (SQLITE_ROW == query.step()) { | |
458 *username = query.column_string(0); | |
459 return true; | |
460 } else { | |
461 return false; | |
462 } | |
463 } | |
464 | |
465 } // namespace browser_sync | |
OLD | NEW |