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