/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "passport/passport_form_controller.h" #include "passport/passport_form_box.h" #include "passport/passport_edit_identity_box.h" #include "passport/passport_encryption.h" #include "boxes/confirm_box.h" #include "lang/lang_keys.h" #include "base/openssl_help.h" #include "mainwindow.h" #include "auth_session.h" #include "storage/localimageloader.h" #include "storage/file_upload.h" #include "storage/file_download.h" namespace Passport { FormRequest::FormRequest( UserId botId, const QString &scope, const QString &callbackUrl, const QString &publicKey) : botId(botId) , scope(scope) , callbackUrl(callbackUrl) , publicKey(publicKey) { } FormController::UploadedScan::~UploadedScan() { if (fullId) { Auth().uploader().cancel(fullId); } } FormController::EditFile::EditFile( const File &fields, std::unique_ptr &&uploaded) : fields(std::move(fields)) , uploaded(std::move(uploaded)) { } FormController::Field::Field(Type type) : type(type) { } template bytes::vector FormController::computeFilesHash( FileHashes fileHashes, bytes::const_span valueSecret) { auto vec = bytes::concatenate(fileHashes); auto hashesVector = std::vector(); for (const auto &hash : fileHashes) { hashesVector.push_back(bytes::make_span(hash)); } return PrepareFilesHash(hashesVector, valueSecret); } FormController::FormController( not_null controller, const FormRequest &request) : _controller(controller) , _request(request) { } void FormController::show() { requestForm(); requestPassword(); } bytes::vector FormController::passwordHashForAuth( bytes::const_span password) const { return openssl::Sha256(bytes::concatenate( _password.salt, password, _password.salt)); } void FormController::submitPassword(const QString &password) { Expects(!_password.salt.empty()); if (_passwordCheckRequestId) { return; } else if (password.isEmpty()) { _passwordError.fire(QString()); } const auto passwordBytes = password.toUtf8(); _passwordCheckRequestId = request(MTPaccount_GetPasswordSettings( MTP_bytes(passwordHashForAuth(bytes::make_span(passwordBytes))) )).handleFloodErrors( ).done([=](const MTPaccount_PasswordSettings &result) { Expects(result.type() == mtpc_account_passwordSettings); _passwordCheckRequestId = 0; const auto &data = result.c_account_passwordSettings(); _password.confirmedEmail = qs(data.vemail); validateSecureSecret( bytes::make_span(data.vsecure_salt.v), bytes::make_span(data.vsecure_secret.v), bytes::make_span(passwordBytes)); }).fail([=](const RPCError &error) { _passwordCheckRequestId = 0; if (MTP::isFloodError(error)) { _passwordError.fire(lang(lng_flood_error)); } else if (error.type() == qstr("PASSWORD_HASH_INVALID")) { _passwordError.fire(lang(lng_passport_password_wrong)); } else { _passwordError.fire_copy(error.type()); } }).send(); } void FormController::validateSecureSecret( bytes::const_span salt, bytes::const_span encryptedSecret, bytes::const_span password) { if (!salt.empty() && !encryptedSecret.empty()) { _secret = DecryptSecureSecret(salt, encryptedSecret, password); if (_secret.empty()) { LOG(("API Error: Failed to decrypt secure secret. " "Forgetting all files and data :(")); for (auto &field : _form.fields) { if (!field.encryptedSecret.empty()) { resetField(field); } } } else { decryptFields(); } } if (_secret.empty()) { generateSecret(password); } _secretReady.fire({}); } void FormController::decryptFields() { Expects(!_secret.empty()); for (auto &field : _form.fields) { if (field.encryptedSecret.empty()) { continue; } decryptField(field); } } void FormController::decryptField(Field &field) { Expects(!_secret.empty()); Expects(!field.encryptedSecret.empty()); if (!validateFieldSecret(field)) { resetField(field); return; } const auto dataHash = bytes::make_span(field.dataHash); field.parsedData = DeserializeData(DecryptData( bytes::make_span(field.originalData), field.dataHash, field.secret)); } bool FormController::validateFieldSecret(Field &field) { field.secret = DecryptValueSecret( field.encryptedSecret, _secret, field.hash); if (field.secret.empty()) { LOG(("API Error: Could not decrypt value secret. " "Forgetting files and data :(")); return false; } const auto fileHashes = ranges::view::all( field.files ) | ranges::view::transform([](File &file) { return bytes::make_span(file.fileHash); }); const auto countedHash = openssl::Sha256(bytes::concatenate( field.dataHash, bytes::concatenate(fileHashes), field.secret)); if (field.hash != countedHash) { LOG(("API Error: Wrong hash after decrypting value secret. " "Forgetting files and data :(")); return false; } return true; } void FormController::resetField(Field &field) { field = Field(field.type); } rpl::producer FormController::passwordError() const { return _passwordError.events(); } QString FormController::passwordHint() const { return _password.hint; } void FormController::uploadScan(int index, QByteArray &&content) { Expects(_editBox != nullptr); Expects(index >= 0 && index < _form.fields.size()); auto &field = _form.fields[index]; if (field.secret.empty()) { field.secret = GenerateSecretBytes(); } const auto weak = _editBox; crl::async([ =, bytes = std::move(content), valueSecret = field.secret ] { auto data = EncryptData( bytes::make_span(bytes), valueSecret); auto result = UploadedScan(); result.fileId = rand_value(); result.hash = std::move(data.hash); result.bytes = std::move(data.bytes); result.md5checksum.resize(32); hashMd5Hex( result.bytes.data(), result.bytes.size(), result.md5checksum.data()); crl::on_main([=, encrypted = std::move(result)]() mutable { if (weak) { uploadEncryptedScan(index, std::move(encrypted)); } }); }); } void FormController::subscribeToUploader() { if (_uploaderSubscriptions) { return; } Auth().uploader().secureReady( ) | rpl::start_with_next([=](const Storage::UploadedSecure &data) { scanUploaded(data); }, _uploaderSubscriptions); Auth().uploader().secureProgress( ) | rpl::start_with_next([=](const FullMsgId &fullId) { }, _uploaderSubscriptions); Auth().uploader().secureFailed( ) | rpl::start_with_next([=](const FullMsgId &fullId) { }, _uploaderSubscriptions); } void FormController::uploadEncryptedScan(int index, UploadedScan &&data) { Expects(_editBox != nullptr); Expects(index >= 0 && index < _form.fields.size()); subscribeToUploader(); _form.fields[index].filesInEdit.emplace_back( File(), std::make_unique(std::move(data))); auto &file = _form.fields[index].filesInEdit.back(); auto uploaded = std::make_shared( TaskId(), file.uploaded->fileId, FileLoadTo(PeerId(0), false, MsgId(0)), TextWithTags(), std::shared_ptr(nullptr)); uploaded->type = SendMediaType::Secure; uploaded->content = QByteArray::fromRawData( reinterpret_cast(file.uploaded->bytes.data()), file.uploaded->bytes.size()); uploaded->setFileData(uploaded->content); uploaded->filemd5 = file.uploaded->md5checksum; file.uploaded->fullId = FullMsgId(0, clientMsgId()); Auth().uploader().upload(file.uploaded->fullId, std::move(uploaded)); } void FormController::scanUploaded(const Storage::UploadedSecure &data) { if (const auto edit = findEditFile(data.fullId)) { Assert(edit->uploaded != nullptr); edit->fields.id = edit->uploaded->fileId = data.fileId; edit->fields.size = edit->uploaded->bytes.size(); edit->fields.dcId = MTP::maindc(); edit->uploaded->partsCount = data.partsCount; edit->fields.bytes = std::move(edit->uploaded->bytes); edit->fields.fileHash = std::move(edit->uploaded->hash); edit->uploaded->fullId = FullMsgId(); } } rpl::producer<> FormController::secretReadyEvents() const { return _secretReady.events(); } QString FormController::defaultEmail() const { return _password.confirmedEmail; } QString FormController::defaultPhoneNumber() const { if (const auto self = App::self()) { return self->phone(); } return QString(); } rpl::producer FormController::scanUpdated() const { return _scanUpdated.events(); } void FormController::fillRows( base::lambda callback) { for (const auto &field : _form.fields) { switch (field.type) { case Field::Type::Identity: callback( lang(lng_passport_identity_title), lang(lng_passport_identity_description), false); break; case Field::Type::Address: callback( lang(lng_passport_address_title), lang(lng_passport_address_description), false); break; case Field::Type::Phone: callback( lang(lng_passport_phone_title), App::self()->phone(), true); break; case Field::Type::Email: callback( lang(lng_passport_email_title), lang(lng_passport_email_description), false); break; } } } void FormController::editField(int index) { Expects(index >= 0 && index < _form.fields.size()); auto box = [&]() -> object_ptr { auto &field = _form.fields[index]; switch (field.type) { case Field::Type::Identity: loadFiles(field.files); field.filesInEdit = ranges::view::all( field.files ) | ranges::view::transform([](const File &file) { return EditFile(file, nullptr); }) | ranges::to_vector; return Box( this, index, fieldDataIdentity(field), fieldFilesIdentity(field)); } return { nullptr }; }(); if (box) { _editBox = Ui::show(std::move(box), LayerOption::KeepOther); } } void FormController::loadFiles(const std::vector &files) { for (const auto &file : files) { const auto key = FileKey{ file.id, file.dcId }; const auto i = _fileLoaders.find(key); if (i == _fileLoaders.end()) { const auto [i, ok] = _fileLoaders.emplace( key, std::make_unique( file.dcId, file.id, file.accessHash, 0, SecureFileLocation, QString(), file.size, LoadToCacheAsWell, LoadFromCloudOrLocal, false)); const auto loader = i->second.get(); loader->connect(loader, &mtpFileLoader::progress, [=] { if (loader->finished()) { fileLoaded(key, loader->bytes()); } }); loader->connect(loader, &mtpFileLoader::failed, [=] { }); loader->start(); } } } void FormController::fileLoaded(FileKey key, const QByteArray &bytes) { if (const auto [field, file] = findFile(key); file != nullptr) { const auto decrypted = DecryptData( bytes::make_span(bytes), file->fileHash, field->secret); auto image = App::readImage(QByteArray::fromRawData( reinterpret_cast(decrypted.data()), decrypted.size())); if (!image.isNull()) { _scanUpdated.fire({ FileKey{ file->id, file->dcId }, QString("loaded"), std::move(image), }); } } } IdentityData FormController::fieldDataIdentity(const Field &field) const { const auto &map = field.parsedData; auto result = IdentityData(); if (const auto i = map.find(qsl("first_name")); i != map.cend()) { result.name = i->second; } if (const auto i = map.find(qsl("last_name")); i != map.cend()) { result.surname = i->second; } return result; } std::vector FormController::fieldFilesIdentity( const Field &field) const { auto result = std::vector(); for (const auto &file : field.filesInEdit) { result.push_back({ FileKey{ file.fields.id, file.fields.dcId }, langDateTime(QDateTime::currentDateTime()), QImage() }); } return result; } void FormController::saveFieldIdentity( int index, const IdentityData &data) { Expects(_editBox != nullptr); Expects(index >= 0 && index < _form.fields.size()); Expects(_form.fields[index].type == Field::Type::Identity); _form.fields[index].parsedData[qsl("first_name")] = data.name; _form.fields[index].parsedData[qsl("last_name")] = data.surname; saveIdentity(index); } void FormController::saveIdentity(int index) { Expects(index >= 0 && index < _form.fields.size()); Expects(_form.fields[index].type == Field::Type::Identity); if (_secret.empty()) { _secretCallbacks.push_back([=] { saveIdentity(index); }); return; } _editBox->closeBox(); auto &field = _form.fields[index]; auto inputFiles = QVector(); inputFiles.reserve(field.filesInEdit.size()); for (const auto &file : field.filesInEdit) { if (const auto uploaded = file.uploaded.get()) { inputFiles.push_back(MTP_inputSecureFileUploaded( MTP_long(file.fields.id), MTP_int(uploaded->partsCount), MTP_bytes(uploaded->md5checksum), MTP_bytes(file.fields.fileHash))); } else { inputFiles.push_back(MTP_inputSecureFile( MTP_long(file.fields.id), MTP_long(file.fields.accessHash))); } } if (field.secret.empty()) { field.secret = GenerateSecretBytes(); } const auto encryptedData = EncryptData( SerializeData(field.parsedData), field.secret); const auto fileHashes = ranges::view::all( field.filesInEdit ) | ranges::view::transform([](const EditFile &file) { return bytes::make_span(file.fields.fileHash); }); const auto valueHash = openssl::Sha256(bytes::concatenate( encryptedData.hash, bytes::concatenate(fileHashes), field.secret)); field.encryptedSecret = EncryptValueSecret( field.secret, _secret, valueHash); request(MTPaccount_SaveSecureValue( MTP_inputSecureValueIdentity( MTP_secureData( MTP_bytes(encryptedData.bytes), MTP_bytes(encryptedData.hash)), MTP_vector(inputFiles), MTP_bytes(field.encryptedSecret), MTP_bytes(valueHash)), MTP_long(CountSecureSecretHash(_secret)) )).done([=](const MTPSecureValueSaved &result) { Ui::show(Box("Saved"), LayerOption::KeepOther); }).fail([=](const RPCError &error) { Ui::show(Box("Error saving value.")); }).send(); } void FormController::generateSecret(bytes::const_span password) { if (_saveSecretRequestId) { return; } auto secret = GenerateSecretBytes(); auto randomSaltPart = bytes::vector(8); bytes::set_random(randomSaltPart); auto newSecureSaltFull = bytes::concatenate( _password.newSecureSalt, randomSaltPart); const auto hashForSecret = openssl::Sha512(bytes::concatenate( newSecureSaltFull, password, newSecureSaltFull)); const auto hashForAuth = openssl::Sha256(bytes::concatenate( _password.salt, password, _password.salt)); auto secureSecretHash = CountSecureSecretHash(secret); auto encryptedSecret = EncryptSecretBytes( secret, hashForSecret); using Flag = MTPDaccount_passwordInputSettings::Flag; _saveSecretRequestId = request(MTPaccount_UpdatePasswordSettings( MTP_bytes(hashForAuth), MTP_account_passwordInputSettings( MTP_flags(Flag::f_new_secure_secret), MTPbytes(), // new_salt MTPbytes(), // new_password_hash MTPstring(), // hint MTPstring(), // email MTP_bytes(newSecureSaltFull), MTP_bytes(encryptedSecret), MTP_long(secureSecretHash)) )).done([=](const MTPBool &result) { _saveSecretRequestId = 0; _secret = secret; //_password.salt = newPasswordSaltFull; for (const auto &callback : base::take(_secretCallbacks)) { callback(); } }).fail([=](const RPCError &error) { // #TODO wrong password hash error? Ui::show(Box("Saving encrypted value failed.")); _saveSecretRequestId = 0; }).send(); } void FormController::requestForm() { auto normalizedKey = _request.publicKey; normalizedKey.replace("\r\n", "\n"); _formRequestId = request(MTPaccount_GetAuthorizationForm( MTP_int(_request.botId), MTP_string(_request.scope), MTP_bytes(normalizedKey.toUtf8()) )).done([=](const MTPaccount_AuthorizationForm &result) { _formRequestId = 0; formDone(result); }).fail([=](const RPCError &error) { formFail(error); }).send(); } template auto FormController::parseEncryptedField( Field::Type type, const DataType &data) const -> Field { Expects(data.vdata.type() == mtpc_secureData); auto result = Field(type); if (data.has_verified()) { result.verification = parseVerified(data.vverified); } result.encryptedSecret = bytes::make_vector(data.vsecret.v); result.hash = bytes::make_vector(data.vhash.v); const auto &fields = data.vdata.c_secureData(); result.originalData = fields.vdata.v; result.dataHash = bytes::make_vector(fields.vdata_hash.v); for (const auto &file : data.vfiles.v) { switch (file.type()) { case mtpc_secureFileEmpty: { result.files.push_back(File()); } break; case mtpc_secureFile: { const auto &fields = file.c_secureFile(); auto normal = File(); normal.id = fields.vid.v; normal.accessHash = fields.vaccess_hash.v; normal.size = fields.vsize.v; normal.dcId = fields.vdc_id.v; normal.fileHash = bytes::make_vector(fields.vfile_hash.v); result.files.push_back(std::move(normal)); } break; } } return result; } template auto FormController::parsePlainTextField( Field::Type type, const QByteArray &value, const DataType &data) const -> Field { auto result = Field(type); const auto check = bytes::compare( bytes::make_span(data.vhash.v), openssl::Sha256(bytes::make_span(value))); if (check != 0) { LOG(("API Error: Bad hash for plain text value. " "Value '%1', hash '%2'" ).arg(QString::fromUtf8(value) ).arg(Logs::mb(data.vhash.v.data(), data.vhash.v.size()).str() )); return result; } result.parsedData[QString("value")] = QString::fromUtf8(value); if (data.has_verified()) { result.verification = parseVerified(data.vverified); } result.hash = bytes::make_vector(data.vhash.v); return result; } auto FormController::parseValue( const MTPSecureValue &value) const -> Field { switch (value.type()) { case mtpc_secureValueIdentity: { return parseEncryptedField( Field::Type::Identity, value.c_secureValueIdentity()); } break; case mtpc_secureValueAddress: { return parseEncryptedField( Field::Type::Address, value.c_secureValueAddress()); } break; case mtpc_secureValuePhone: { const auto &data = value.c_secureValuePhone(); return parsePlainTextField( Field::Type::Phone, data.vphone.v, data); } break; case mtpc_secureValueEmail: { const auto &data = value.c_secureValueEmail(); return parsePlainTextField( Field::Type::Phone, data.vemail.v, data); } break; } Unexpected("secureValue type."); } auto FormController::parseVerified(const MTPSecureValueVerified &data) const -> Verification { Expects(data.type() == mtpc_secureValueVerified); const auto &fields = data.c_secureValueVerified(); return Verification{ fields.vdate.v, qs(fields.vprovider) }; } auto FormController::findEditFile(const FullMsgId &fullId) -> EditFile* { for (auto &field : _form.fields) { for (auto &file : field.filesInEdit) { if (file.uploaded && file.uploaded->fullId == fullId) { return &file; } } } return nullptr; } auto FormController::findFile(const FileKey &key) -> std::pair { for (auto &field : _form.fields) { for (auto &file : field.files) { if (file.dcId == key.dcId && file.id == key.id) { return { &field, &file }; } } } return { nullptr, nullptr }; } void FormController::formDone(const MTPaccount_AuthorizationForm &result) { parseForm(result); if (!_passwordRequestId) { showForm(); } } void FormController::parseForm(const MTPaccount_AuthorizationForm &result) { Expects(result.type() == mtpc_account_authorizationForm); const auto &data = result.c_account_authorizationForm(); auto values = std::vector(); for (const auto &value : data.vvalues.v) { values.push_back(parseValue(value)); } const auto findValue = [&](Field::Type type) -> Field* { for (auto &value : values) { if (value.type == type) { return &value; } } return nullptr; }; _form.requestWrite = false; App::feedUsers(data.vusers); for (const auto &required : data.vrequired_types.v) { using Type = Field::Type; const auto type = [&] { switch (required.type()) { case mtpc_secureValueTypeIdentity: return Type::Identity; case mtpc_secureValueTypeAddress: return Type::Address; case mtpc_secureValueTypeEmail: return Type::Email; case mtpc_secureValueTypePhone: return Type::Phone; } Unexpected("Type in secureValueType type."); }(); if (auto field = findValue(type)) { _form.fields.push_back(std::move(*field)); } else { _form.fields.push_back(Field(type)); } } _bot = App::userLoaded(_request.botId); } void FormController::showForm() { if (!_bot) { Ui::show(Box("Could not get authorization bot.")); return; } Ui::show(Box(this)); } void FormController::formFail(const RPCError &error) { Ui::show(Box(lang(lng_passport_form_error))); } void FormController::requestPassword() { _passwordRequestId = request(MTPaccount_GetPassword( )).done([=](const MTPaccount_Password &result) { _passwordRequestId = 0; passwordDone(result); }).fail([=](const RPCError &error) { formFail(error); }).send(); } void FormController::passwordDone(const MTPaccount_Password &result) { switch (result.type()) { case mtpc_account_noPassword: parsePassword(result.c_account_noPassword()); break; case mtpc_account_password: parsePassword(result.c_account_password()); break; } if (!_formRequestId) { showForm(); } } void FormController::passwordFail(const RPCError &error) { Ui::show(Box("Could not get authorization form.")); } void FormController::parsePassword(const MTPDaccount_noPassword &result) { _password.unconfirmedPattern = qs(result.vemail_unconfirmed_pattern); _password.newSalt = bytes::make_vector(result.vnew_salt.v); _password.newSecureSalt = bytes::make_vector(result.vnew_secure_salt.v); openssl::AddRandomSeed(bytes::make_span(result.vsecure_random.v)); } void FormController::parsePassword(const MTPDaccount_password &result) { _password.hint = qs(result.vhint); _password.hasRecovery = mtpIsTrue(result.vhas_recovery); _password.salt = bytes::make_vector(result.vcurrent_salt.v); _password.unconfirmedPattern = qs(result.vemail_unconfirmed_pattern); _password.newSalt = bytes::make_vector(result.vnew_salt.v); _password.newSecureSalt = bytes::make_vector(result.vnew_secure_salt.v); openssl::AddRandomSeed(bytes::make_span(result.vsecure_random.v)); } FormController::~FormController() = default; } // namespace Passport