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