1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "storage/details/storage_file_utilities.h"
9 
10 #include "mtproto/mtproto_auth_key.h"
11 #include "base/platform/base_platform_file_utilities.h"
12 #include "base/openssl_help.h"
13 #include "base/random.h"
14 
15 #include <crl/crl_object_on_thread.h>
16 #include <QtCore/QtEndian>
17 #include <QtCore/QSaveFile>
18 
19 namespace Storage {
20 namespace details {
21 namespace {
22 
23 constexpr char TdfMagic[] = { 'T', 'D', 'F', '$' };
24 constexpr auto TdfMagicLen = int(sizeof(TdfMagic));
25 
26 constexpr auto kStrongIterationsCount = 100'000;
27 
28 struct WriteEntry {
29 	QString basePath;
30 	QString base;
31 	QByteArray data;
32 	QByteArray md5;
33 };
34 
35 class WriteManager final {
36 public:
37 	explicit WriteManager(crl::weak_on_thread<WriteManager> weak);
38 
39 	void write(WriteEntry &&entry);
40 	void writeSync(WriteEntry &&entry);
41 	void writeSyncAll();
42 
43 private:
44 	void scheduleWrite();
45 	void writeScheduled();
46 	bool writeOneScheduledNow();
47 	void writeNow(WriteEntry &&entry);
48 
49 	template <typename File>
50 	[[nodiscard]] bool open(File &file, const WriteEntry &entry, char postfix);
51 
52 	[[nodiscard]] QString path(const WriteEntry &entry, char postfix) const;
53 	[[nodiscard]] bool writeHeader(
54 		const QString &basePath,
55 		QFileDevice &file);
56 
57 	crl::weak_on_thread<WriteManager> _weak;
58 	std::deque<WriteEntry> _scheduled;
59 
60 };
61 
62 class AsyncWriteManager final {
63 public:
64 	void write(WriteEntry &&entry);
65 	void writeSync(WriteEntry &&entry);
66 	void sync();
67 	void stop();
68 
69 private:
70 	std::optional<crl::object_on_thread<WriteManager>> _manager;
71 	bool _finished = false;
72 
73 };
74 
WriteManager(crl::weak_on_thread<WriteManager> weak)75 WriteManager::WriteManager(crl::weak_on_thread<WriteManager> weak)
76 : _weak(std::move(weak)) {
77 }
78 
write(WriteEntry && entry)79 void WriteManager::write(WriteEntry &&entry) {
80 	const auto i = ranges::find(_scheduled, entry.base, &WriteEntry::base);
81 	if (i == end(_scheduled)) {
82 		_scheduled.push_back(std::move(entry));
83 	} else {
84 		*i = std::move(entry);
85 	}
86 	scheduleWrite();
87 }
88 
writeSync(WriteEntry && entry)89 void WriteManager::writeSync(WriteEntry &&entry) {
90 	const auto i = ranges::find(_scheduled, entry.base, &WriteEntry::base);
91 	if (i != end(_scheduled)) {
92 		_scheduled.erase(i);
93 	}
94 	writeNow(std::move(entry));
95 }
96 
writeNow(WriteEntry && entry)97 void WriteManager::writeNow(WriteEntry &&entry) {
98 	const auto path = [&](char postfix) {
99 		return this->path(entry, postfix);
100 	};
101 	const auto open = [&](auto &file, char postfix) {
102 		return this->open(file, entry, postfix);
103 	};
104 	const auto write = [&](auto &file) {
105 		file.write(entry.data);
106 		file.write(entry.md5);
107 	};
108 	const auto safe = path('s');
109 	const auto simple = path('0');
110 	const auto backup = path('1');
111 	QSaveFile save;
112 	if (open(save, 's')) {
113 		write(save);
114 		if (save.commit()) {
115 			QFile::remove(simple);
116 			QFile::remove(backup);
117 			return;
118 		}
119 		LOG(("Storage Error: Could not commit '%1'.").arg(safe));
120 	}
121 	QFile plain;
122 	if (open(plain, '0')) {
123 		write(plain);
124 		base::Platform::FlushFileData(plain);
125 		plain.close();
126 
127 		QFile::remove(backup);
128 		if (base::Platform::RenameWithOverwrite(simple, safe)) {
129 			return;
130 		}
131 		QFile::remove(safe);
132 		LOG(("Storage Error: Could not rename '%1' to '%2', removing.").arg(
133 			simple,
134 			safe));
135 	}
136 }
137 
writeSyncAll()138 void WriteManager::writeSyncAll() {
139 	while (writeOneScheduledNow()) {
140 	}
141 }
142 
writeOneScheduledNow()143 bool WriteManager::writeOneScheduledNow() {
144 	if (_scheduled.empty()) {
145 		return false;
146 	}
147 
148 	auto entry = std::move(_scheduled.front());
149 	_scheduled.pop_front();
150 
151 	writeNow(std::move(entry));
152 	return true;
153 }
154 
writeHeader(const QString & basePath,QFileDevice & file)155 bool WriteManager::writeHeader(const QString &basePath, QFileDevice &file) {
156 	if (!file.open(QIODevice::WriteOnly)) {
157 		const auto dir = QDir(basePath);
158 		if (dir.exists()) {
159 			return false;
160 		} else if (!QDir().mkpath(dir.absolutePath())) {
161 			return false;
162 		} else if (!file.open(QIODevice::WriteOnly)) {
163 			return false;
164 		}
165 	}
166 	file.write(TdfMagic, TdfMagicLen);
167 	const auto version = qint32(AppVersion);
168 	file.write((const char*)&version, sizeof(version));
169 	return true;
170 }
171 
path(const WriteEntry & entry,char postfix) const172 QString WriteManager::path(const WriteEntry &entry, char postfix) const {
173 	return entry.base + postfix;
174 }
175 
176 template <typename File>
open(File & file,const WriteEntry & entry,char postfix)177 bool WriteManager::open(File &file, const WriteEntry &entry, char postfix) {
178 	const auto name = path(entry, postfix);
179 	file.setFileName(name);
180 	if (!writeHeader(entry.basePath, file)) {
181 		LOG(("Storage Error: Could not open '%1' for writing.").arg(name));
182 		return false;
183 	}
184 	return true;
185 }
186 
scheduleWrite()187 void WriteManager::scheduleWrite() {
188 	_weak.with([](WriteManager &that) {
189 		that.writeScheduled();
190 	});
191 }
192 
writeScheduled()193 void WriteManager::writeScheduled() {
194 	if (writeOneScheduledNow() && !_scheduled.empty()) {
195 		scheduleWrite();
196 	}
197 }
198 
write(WriteEntry && entry)199 void AsyncWriteManager::write(WriteEntry &&entry) {
200 	Expects(!_finished);
201 
202 	if (!_manager) {
203 		_manager.emplace();
204 	}
205 	_manager->with([entry = std::move(entry)](WriteManager &manager) mutable {
206 		manager.write(std::move(entry));
207 	});
208 }
209 
writeSync(WriteEntry && entry)210 void AsyncWriteManager::writeSync(WriteEntry &&entry) {
211 	Expects(!_finished);
212 
213 	if (!_manager) {
214 		_manager.emplace();
215 	}
216 	_manager->with_sync([&](WriteManager &manager) {
217 		manager.writeSync(std::move(entry));
218 	});
219 }
220 
sync()221 void AsyncWriteManager::sync() {
222 	if (_manager) {
223 		_manager->with_sync([](WriteManager &manager) {
224 			manager.writeSyncAll();
225 		});
226 	}
227 }
228 
stop()229 void AsyncWriteManager::stop() {
230 	if (_manager) {
231 		sync();
232 		_manager.reset();
233 	}
234 	_finished = true;
235 }
236 
237 AsyncWriteManager Manager;
238 
239 } // namespace
240 
ToFilePart(FileKey val)241 QString ToFilePart(FileKey val) {
242 	QString result;
243 	result.reserve(0x10);
244 	for (int32 i = 0; i < 0x10; ++i) {
245 		uchar v = (val & 0x0F);
246 		result.push_back((v < 0x0A) ? ('0' + v) : ('A' + (v - 0x0A)));
247 		val >>= 4;
248 	}
249 	return result;
250 }
251 
KeyAlreadyUsed(QString & name)252 bool KeyAlreadyUsed(QString &name) {
253 	name += '0';
254 	if (QFileInfo::exists(name)) {
255 		return true;
256 	}
257 	name[name.size() - 1] = '1';
258 	if (QFileInfo::exists(name)) {
259 		return true;
260 	}
261 	name[name.size() - 1] = 's';
262 	if (QFileInfo::exists(name)) {
263 		return true;
264 	}
265 	return false;
266 }
267 
GenerateKey(const QString & basePath)268 FileKey GenerateKey(const QString &basePath) {
269 	FileKey result;
270 	QString path;
271 	path.reserve(basePath.size() + 0x11);
272 	path += basePath;
273 	do {
274 		result = base::RandomValue<FileKey>();
275 		path.resize(basePath.size());
276 		path += ToFilePart(result);
277 	} while (!result || KeyAlreadyUsed(path));
278 
279 	return result;
280 }
281 
ClearKey(const FileKey & key,const QString & basePath)282 void ClearKey(const FileKey &key, const QString &basePath) {
283 	QString name;
284 	name.reserve(basePath.size() + 0x11);
285 	name.append(basePath).append(ToFilePart(key)).append('0');
286 	QFile::remove(name);
287 	name[name.size() - 1] = '1';
288 	QFile::remove(name);
289 	name[name.size() - 1] = 's';
290 	QFile::remove(name);
291 }
292 
CheckStreamStatus(QDataStream & stream)293 bool CheckStreamStatus(QDataStream &stream) {
294 	if (stream.status() != QDataStream::Ok) {
295 		LOG(("Bad data stream status: %1").arg(stream.status()));
296 		return false;
297 	}
298 	return true;
299 }
300 
CreateLocalKey(const QByteArray & passcode,const QByteArray & salt)301 MTP::AuthKeyPtr CreateLocalKey(
302 		const QByteArray &passcode,
303 		const QByteArray &salt) {
304 	const auto s = bytes::make_span(salt);
305 	const auto hash = openssl::Sha512(s, bytes::make_span(passcode), s);
306 	const auto iterationsCount = passcode.isEmpty()
307 		? 1 // Don't slow down for no password.
308 		: kStrongIterationsCount;
309 
310 	auto key = MTP::AuthKey::Data{ { gsl::byte{} } };
311 	PKCS5_PBKDF2_HMAC(
312 		reinterpret_cast<const char*>(hash.data()),
313 		hash.size(),
314 		reinterpret_cast<const unsigned char*>(s.data()),
315 		s.size(),
316 		iterationsCount,
317 		EVP_sha512(),
318 		key.size(),
319 		reinterpret_cast<unsigned char*>(key.data()));
320 	return std::make_shared<MTP::AuthKey>(key);
321 }
322 
CreateLegacyLocalKey(const QByteArray & passcode,const QByteArray & salt)323 MTP::AuthKeyPtr CreateLegacyLocalKey(
324 		const QByteArray &passcode,
325 		const QByteArray &salt) {
326 	auto key = MTP::AuthKey::Data{ { gsl::byte{} } };
327 	const auto iterationsCount = passcode.isEmpty()
328 		? LocalEncryptNoPwdIterCount // Don't slow down for no password.
329 		: LocalEncryptIterCount;
330 
331 	PKCS5_PBKDF2_HMAC_SHA1(
332 		passcode.constData(),
333 		passcode.size(),
334 		(uchar*)salt.data(),
335 		salt.size(),
336 		iterationsCount,
337 		key.size(),
338 		(uchar*)key.data());
339 
340 	return std::make_shared<MTP::AuthKey>(key);
341 }
342 
~FileReadDescriptor()343 FileReadDescriptor::~FileReadDescriptor() {
344 	if (version) {
345 		stream.setDevice(nullptr);
346 		if (buffer.isOpen()) {
347 			buffer.close();
348 		}
349 		buffer.setBuffer(nullptr);
350 	}
351 }
352 
EncryptedDescriptor()353 EncryptedDescriptor::EncryptedDescriptor() {
354 }
355 
EncryptedDescriptor(uint32 size)356 EncryptedDescriptor::EncryptedDescriptor(uint32 size) {
357 	uint32 fullSize = sizeof(uint32) + size;
358 	if (fullSize & 0x0F) fullSize += 0x10 - (fullSize & 0x0F);
359 	data.reserve(fullSize);
360 
361 	data.resize(sizeof(uint32));
362 	buffer.setBuffer(&data);
363 	buffer.open(QIODevice::WriteOnly);
364 	buffer.seek(sizeof(uint32));
365 	stream.setDevice(&buffer);
366 	stream.setVersion(QDataStream::Qt_5_1);
367 }
368 
~EncryptedDescriptor()369 EncryptedDescriptor::~EncryptedDescriptor() {
370 	finish();
371 }
372 
finish()373 void EncryptedDescriptor::finish() {
374 	if (stream.device()) stream.setDevice(nullptr);
375 	if (buffer.isOpen()) buffer.close();
376 	buffer.setBuffer(nullptr);
377 }
378 
FileWriteDescriptor(const FileKey & key,const QString & basePath,bool sync)379 FileWriteDescriptor::FileWriteDescriptor(
380 	const FileKey &key,
381 	const QString &basePath,
382 	bool sync)
383 : FileWriteDescriptor(ToFilePart(key), basePath, sync) {
384 }
385 
FileWriteDescriptor(const QString & name,const QString & basePath,bool sync)386 FileWriteDescriptor::FileWriteDescriptor(
387 	const QString &name,
388 	const QString &basePath,
389 	bool sync)
390 : _basePath(basePath)
391 , _sync(sync) {
392 	init(name);
393 }
394 
~FileWriteDescriptor()395 FileWriteDescriptor::~FileWriteDescriptor() {
396 	finish();
397 }
398 
init(const QString & name)399 void FileWriteDescriptor::init(const QString &name) {
400 	_base = _basePath + name;
401 	_buffer.setBuffer(&_safeData);
402 	const auto opened = _buffer.open(QIODevice::WriteOnly);
403 	Assert(opened);
404 	_stream.setDevice(&_buffer);
405 }
406 
writeData(const QByteArray & data)407 void FileWriteDescriptor::writeData(const QByteArray &data) {
408 	if (!_stream.device()) {
409 		return;
410 	}
411 	_stream << data;
412 	quint32 len = data.isNull() ? 0xffffffff : data.size();
413 	if (QSysInfo::ByteOrder != QSysInfo::BigEndian) {
414 		len = qbswap(len);
415 	}
416 	_md5.feed(&len, sizeof(len));
417 	_md5.feed(data.constData(), data.size());
418 	_fullSize += sizeof(len) + data.size();
419 }
420 
writeEncrypted(EncryptedDescriptor & data,const MTP::AuthKeyPtr & key)421 void FileWriteDescriptor::writeEncrypted(
422 	EncryptedDescriptor &data,
423 	const MTP::AuthKeyPtr &key) {
424 	writeData(PrepareEncrypted(data, key));
425 }
426 
finish()427 void FileWriteDescriptor::finish() {
428 	if (!_stream.device()) {
429 		return;
430 	}
431 
432 	_stream.setDevice(nullptr);
433 	_md5.feed(&_fullSize, sizeof(_fullSize));
434 	qint32 version = AppVersion;
435 	_md5.feed(&version, sizeof(version));
436 	_md5.feed(TdfMagic, TdfMagicLen);
437 
438 	_buffer.close();
439 
440 	auto entry = WriteEntry{
441 		.basePath = _basePath,
442 		.base = _base,
443 		.data = _safeData,
444 		.md5 = QByteArray((const char*)_md5.result(), 0x10)
445 	};
446 	if (_sync) {
447 		Manager.writeSync(std::move(entry));
448 	} else {
449 		Manager.write(std::move(entry));
450 	}
451 }
452 
PrepareEncrypted(EncryptedDescriptor & data,const MTP::AuthKeyPtr & key)453 [[nodiscard]] QByteArray PrepareEncrypted(
454 		EncryptedDescriptor &data,
455 		const MTP::AuthKeyPtr &key) {
456 	data.finish();
457 	QByteArray &toEncrypt(data.data);
458 
459 	// prepare for encryption
460 	uint32 size = toEncrypt.size(), fullSize = size;
461 	if (fullSize & 0x0F) {
462 		fullSize += 0x10 - (fullSize & 0x0F);
463 		toEncrypt.resize(fullSize);
464 		base::RandomFill(toEncrypt.data() + size, fullSize - size);
465 	}
466 	*(uint32*)toEncrypt.data() = size;
467 	QByteArray encrypted(0x10 + fullSize, Qt::Uninitialized); // 128bit of sha1 - key128, sizeof(data), data
468 	hashSha1(toEncrypt.constData(), toEncrypt.size(), encrypted.data());
469 	MTP::aesEncryptLocal(toEncrypt.constData(), encrypted.data() + 0x10, fullSize, key, encrypted.constData());
470 
471 	return encrypted;
472 }
473 
ReadFile(FileReadDescriptor & result,const QString & name,const QString & basePath)474 bool ReadFile(
475 		FileReadDescriptor &result,
476 		const QString &name,
477 		const QString &basePath) {
478 	const auto base = basePath + name;
479 
480 	// detect order of read attempts
481 	QString toTry[2];
482 	const auto modern = base + 's';
483 	if (QFileInfo::exists(modern)) {
484 		toTry[0] = modern;
485 	} else {
486 		// Legacy way.
487 		toTry[0] = base + '0';
488 		QFileInfo toTry0(toTry[0]);
489 		if (toTry0.exists()) {
490 			toTry[1] = basePath + name + '1';
491 			QFileInfo toTry1(toTry[1]);
492 			if (toTry1.exists()) {
493 				QDateTime mod0 = toTry0.lastModified();
494 				QDateTime mod1 = toTry1.lastModified();
495 				if (mod0 < mod1) {
496 					qSwap(toTry[0], toTry[1]);
497 				}
498 			} else {
499 				toTry[1] = QString();
500 			}
501 		} else {
502 			toTry[0][toTry[0].size() - 1] = '1';
503 		}
504 	}
505 	for (int32 i = 0; i < 2; ++i) {
506 		QString fname(toTry[i]);
507 		if (fname.isEmpty()) break;
508 
509 		QFile f(fname);
510 		if (!f.open(QIODevice::ReadOnly)) {
511 			DEBUG_LOG(("App Info: failed to open '%1' for reading"
512 				).arg(name));
513 			continue;
514 		}
515 
516 		// check magic
517 		char magic[TdfMagicLen];
518 		if (f.read(magic, TdfMagicLen) != TdfMagicLen) {
519 			DEBUG_LOG(("App Info: failed to read magic from '%1'"
520 				).arg(name));
521 			continue;
522 		}
523 		if (memcmp(magic, TdfMagic, TdfMagicLen)) {
524 			DEBUG_LOG(("App Info: bad magic %1 in '%2'").arg(
525 				Logs::mb(magic, TdfMagicLen).str(),
526 				name));
527 			continue;
528 		}
529 
530 		// read app version
531 		qint32 version;
532 		if (f.read((char*)&version, sizeof(version)) != sizeof(version)) {
533 			DEBUG_LOG(("App Info: failed to read version from '%1'"
534 				).arg(name));
535 			continue;
536 		}
537 		if (version > AppVersion) {
538 			DEBUG_LOG(("App Info: version too big %1 for '%2', my version %3"
539 				).arg(version
540 				).arg(name
541 				).arg(AppVersion));
542 			continue;
543 		}
544 
545 		// read data
546 		QByteArray bytes = f.read(f.size());
547 		int32 dataSize = bytes.size() - 16;
548 		if (dataSize < 0) {
549 			DEBUG_LOG(("App Info: bad file '%1', could not read sign part"
550 				).arg(name));
551 			continue;
552 		}
553 
554 		// check signature
555 		HashMd5 md5;
556 		md5.feed(bytes.constData(), dataSize);
557 		md5.feed(&dataSize, sizeof(dataSize));
558 		md5.feed(&version, sizeof(version));
559 		md5.feed(magic, TdfMagicLen);
560 		if (memcmp(md5.result(), bytes.constData() + dataSize, 16)) {
561 			DEBUG_LOG(("App Info: bad file '%1', signature did not match"
562 				).arg(name));
563 			continue;
564 		}
565 
566 		bytes.resize(dataSize);
567 		result.data = bytes;
568 		bytes = QByteArray();
569 
570 		result.version = version;
571 		result.buffer.setBuffer(&result.data);
572 		result.buffer.open(QIODevice::ReadOnly);
573 		result.stream.setDevice(&result.buffer);
574 		result.stream.setVersion(QDataStream::Qt_5_1);
575 
576 		if ((i == 0 && !toTry[1].isEmpty()) || i == 1) {
577 			QFile::remove(toTry[1 - i]);
578 		}
579 
580 		return true;
581 	}
582 	return false;
583 }
584 
DecryptLocal(EncryptedDescriptor & result,const QByteArray & encrypted,const MTP::AuthKeyPtr & key)585 bool DecryptLocal(
586 		EncryptedDescriptor &result,
587 		const QByteArray &encrypted,
588 		const MTP::AuthKeyPtr &key) {
589 	if (encrypted.size() <= 16 || (encrypted.size() & 0x0F)) {
590 		LOG(("App Error: bad encrypted part size: %1").arg(encrypted.size()));
591 		return false;
592 	}
593 	uint32 fullLen = encrypted.size() - 16;
594 
595 	QByteArray decrypted;
596 	decrypted.resize(fullLen);
597 	const char *encryptedKey = encrypted.constData(), *encryptedData = encrypted.constData() + 16;
598 	aesDecryptLocal(encryptedData, decrypted.data(), fullLen, key, encryptedKey);
599 	uchar sha1Buffer[20];
600 	if (memcmp(hashSha1(decrypted.constData(), decrypted.size(), sha1Buffer), encryptedKey, 16)) {
601 		LOG(("App Info: bad decrypt key, data not decrypted - incorrect password?"));
602 		return false;
603 	}
604 
605 	uint32 dataLen = *(const uint32*)decrypted.constData();
606 	if (dataLen > uint32(decrypted.size()) || dataLen <= fullLen - 16 || dataLen < sizeof(uint32)) {
607 		LOG(("App Error: bad decrypted part size: %1, fullLen: %2, decrypted size: %3").arg(dataLen).arg(fullLen).arg(decrypted.size()));
608 		return false;
609 	}
610 
611 	decrypted.resize(dataLen);
612 	result.data = decrypted;
613 	decrypted = QByteArray();
614 
615 	result.buffer.setBuffer(&result.data);
616 	result.buffer.open(QIODevice::ReadOnly);
617 	result.buffer.seek(sizeof(uint32)); // skip len
618 	result.stream.setDevice(&result.buffer);
619 	result.stream.setVersion(QDataStream::Qt_5_1);
620 
621 	return true;
622 }
623 
ReadEncryptedFile(FileReadDescriptor & result,const QString & name,const QString & basePath,const MTP::AuthKeyPtr & key)624 bool ReadEncryptedFile(
625 		FileReadDescriptor &result,
626 		const QString &name,
627 		const QString &basePath,
628 		const MTP::AuthKeyPtr &key) {
629 	if (!ReadFile(result, name, basePath)) {
630 		return false;
631 	}
632 	QByteArray encrypted;
633 	result.stream >> encrypted;
634 
635 	EncryptedDescriptor data;
636 	if (!DecryptLocal(data, encrypted, key)) {
637 		result.stream.setDevice(nullptr);
638 		if (result.buffer.isOpen()) result.buffer.close();
639 		result.buffer.setBuffer(nullptr);
640 		result.data = QByteArray();
641 		result.version = 0;
642 		return false;
643 	}
644 
645 	result.stream.setDevice(0);
646 	if (result.buffer.isOpen()) {
647 		result.buffer.close();
648 	}
649 	result.buffer.setBuffer(0);
650 	result.data = data.data;
651 	result.buffer.setBuffer(&result.data);
652 	result.buffer.open(QIODevice::ReadOnly);
653 	result.buffer.seek(data.buffer.pos());
654 	result.stream.setDevice(&result.buffer);
655 	result.stream.setVersion(QDataStream::Qt_5_1);
656 
657 	return true;
658 }
659 
ReadEncryptedFile(FileReadDescriptor & result,const FileKey & fkey,const QString & basePath,const MTP::AuthKeyPtr & key)660 bool ReadEncryptedFile(
661 		FileReadDescriptor &result,
662 		const FileKey &fkey,
663 		const QString &basePath,
664 		const MTP::AuthKeyPtr &key) {
665 	return ReadEncryptedFile(result, ToFilePart(fkey), basePath, key);
666 }
667 
Sync()668 void Sync() {
669 	Manager.sync();
670 }
671 
Finish()672 void Finish() {
673 	Manager.stop();
674 }
675 
676 } // namespace details
677 } // namespace Storage
678