3030
3131#include " export_template_manager.h"
3232
33+ #include " core/crypto/crypto.h"
34+ #include " core/crypto/crypto_core.h"
3335#include " core/io/dir_access.h"
3436#include " core/io/json.h"
3537#include " core/io/zip_io.h"
3638#include " core/version.h"
3739#include " editor/editor_node.h"
3840#include " editor/editor_string_names.h"
3941#include " editor/export/editor_export_preset.h"
42+ #include " editor/export/export_template_keys.gen.h"
4043#include " editor/file_system/editor_file_system.h"
4144#include " editor/file_system/editor_paths.h"
4245#include " editor/gui/progress_dialog.h"
4750#include " scene/gui/link_button.h"
4851#include " scene/gui/menu_button.h"
4952#include " scene/gui/option_button.h"
53+ #include " scene/gui/rich_text_label.h"
5054#include " scene/gui/separator.h"
5155#include " scene/gui/tree.h"
5256#include " scene/main/http_request.h"
@@ -239,17 +243,7 @@ void ExportTemplateManager::_download_template_completed(int p_status, int p_cod
239243 String path = download_templates->get_download_file ();
240244
241245 is_downloading_templates = false ;
242- bool ret = _install_file_selected (path, true );
243- if (ret) {
244- // Clean up downloaded file.
245- Ref<DirAccess> da = DirAccess::create (DirAccess::ACCESS_FILESYSTEM);
246- Error err = da->remove (path);
247- if (err != OK) {
248- EditorNode::get_singleton ()->add_io_error (TTR (" Cannot remove temporary file:" ) + " \n " + path + " \n " );
249- }
250- } else {
251- EditorNode::get_singleton ()->add_io_error (vformat (TTR (" Templates installation failed.\n The problematic templates archives can be found at '%s'." ), path));
252- }
246+ _verify_and_install_file_selected (path, true , true );
253247 }
254248 } break ;
255249 }
@@ -426,14 +420,219 @@ void ExportTemplateManager::_install_file() {
426420 install_file_dialog->popup_file_dialog ();
427421}
428422
429- bool ExportTemplateManager::_install_file_selected (const String &p_file, bool p_skip_progress) {
423+ static Vector<uint8_t > _get_current_file_hash (unz_file_info &p_info, unzFile p_pkg) {
424+ CryptoCore::SHA256Context ctx;
425+ Error err = ctx.start ();
426+ ERR_FAIL_COND_V (err != OK, Vector<uint8_t >());
427+
428+ unzOpenCurrentFile (p_pkg);
429+ uint8_t buf[4096 ];
430+ int64_t to_read = p_info.uncompressed_size ;
431+ while (to_read) {
432+ int64_t len = MIN (to_read, 4096 );
433+ int64_t read = unzReadCurrentFile (p_pkg, buf, len);
434+ ERR_FAIL_COND_V (read < 0 , Vector<uint8_t >());
435+ err = ctx.update ((const uint8_t *)buf, read);
436+ ERR_FAIL_COND_V (err != OK, Vector<uint8_t >());
437+ to_read -= read;
438+ if (read == UNZ_EOF) {
439+ break ;
440+ }
441+ }
442+ unzCloseCurrentFile (p_pkg);
443+
444+ Vector<uint8_t > out;
445+ out.resize (32 );
446+ err = ctx.finish ((unsigned char *)out.ptrw ());
447+ ERR_FAIL_COND_V (err != OK, Vector<uint8_t >());
448+
449+ return out;
450+ }
451+
452+ void ExportTemplateManager::_clenup (const String &p_file, bool p_remove_file) {
453+ if (p_file.is_empty ()) {
454+ return ;
455+ }
456+
457+ if (p_remove_file) {
458+ // Clean up downloaded file.
459+ Ref<DirAccess> da = DirAccess::create (DirAccess::ACCESS_FILESYSTEM);
460+ Error err = da->remove (p_file);
461+ if (err != OK) {
462+ EditorNode::get_singleton ()->add_io_error (TTR (" Cannot remove temporary file:" ) + " \n " + p_file + " \n " );
463+ }
464+ }
465+ }
466+
467+ void ExportTemplateManager::_verify_and_install_file_selected (const String &p_file, bool p_skip_progress, bool p_remove_file) {
468+ HashMap<String, Vector<uint8_t >> expected_hashes;
469+ HashMap<String, Vector<uint8_t >> file_hashes;
470+ Vector<uint8_t > signature;
471+ Vector<uint8_t > manifest_hash;
472+ Vector<String> user_keys = EDITOR_GET (" export/template_trusted_public_keys" );
473+ bool has_keys = std::size (trusted_public_keys) > 0 || !user_keys.is_empty ();
474+
430475 Ref<FileAccess> io_fa;
431476 zlib_filefunc_def io = zipio_create_io (&io_fa);
432477
433478 unzFile pkg = unzOpen2 (p_file.utf8 ().get_data (), &io);
434479 if (!pkg) {
435480 EditorNode::get_singleton ()->show_warning (TTR (" Can't open the export templates file." ));
436- return false ;
481+ _clenup (p_file, p_remove_file);
482+ return ;
483+ }
484+
485+ int ret = unzGoToFirstFile (pkg);
486+ while (ret == UNZ_OK) {
487+ unz_file_info info;
488+ char fname[16384 ];
489+ ret = unzGetCurrentFileInfo (pkg, &info, fname, 16384 , nullptr , 0 , nullptr , 0 );
490+ if (ret != UNZ_OK) {
491+ break ;
492+ }
493+ String file = String::utf8 (fname);
494+ if (file == " .manifest" ) {
495+ Vector<uint8_t > uncomp_data;
496+ uncomp_data.resize (info.uncompressed_size );
497+
498+ unzOpenCurrentFile (pkg);
499+ ret = unzReadCurrentFile (pkg, uncomp_data.ptrw (), uncomp_data.size ());
500+ ERR_BREAK (ret < 0 );
501+ unzCloseCurrentFile (pkg);
502+ Vector<String> manifest = String::utf8 ((const char *)uncomp_data.ptr (), uncomp_data.size ()).split (" \n " );
503+ for (const String &file_info : manifest) {
504+ String hash = file_info.get_slice (" " , 0 );
505+ String name = file_info.get_slice (" " , 1 ).trim_prefix (" *" );
506+ if (!name.is_empty () && !hash.is_empty () && name != " .manifest" && name != " .signature" ) {
507+ expected_hashes[name] = hash.hex_decode ();
508+ }
509+ }
510+ manifest_hash = _get_current_file_hash (info, pkg);
511+ } else if (file == " .signature" ) {
512+ signature.resize (info.uncompressed_size );
513+ unzOpenCurrentFile (pkg);
514+ ret = unzReadCurrentFile (pkg, signature.ptrw (), signature.size ());
515+ ERR_BREAK (ret < 0 );
516+ unzCloseCurrentFile (pkg);
517+ } else {
518+ file_hashes[file] = _get_current_file_hash (info, pkg);
519+ }
520+
521+ ret = unzGoToNextFile (pkg);
522+ }
523+ unzClose (pkg);
524+
525+ // Unsigned export templates file.
526+ if (signature.is_empty () && expected_hashes.is_empty ()) {
527+ if (!has_keys) {
528+ // No trusted keys specified, install without user interaction.
529+ _install_file_selected (p_file, p_skip_progress);
530+ _clenup (p_file, p_remove_file);
531+ } else {
532+ // Show confirmation dialog.
533+ ver_file = p_file;
534+ ver_skip_progress = p_skip_progress;
535+ ver_remove_file = p_remove_file;
536+ verification_dialog_accept->popup_centered ();
537+ }
538+ return ;
539+ }
540+
541+ Vector<String> error_msgs;
542+ const String &bullet = U" • " ;
543+
544+ // Verify file hashes.
545+ bool files_ok = true ;
546+ for (const KeyValue<String, Vector<uint8_t >> &E : file_hashes) {
547+ if (!expected_hashes.has (E.key )) {
548+ error_msgs.push_back (bullet + vformat (TTR (" Unexpected file \" %s\" found." ), E.key ));
549+ files_ok = false ;
550+ } else {
551+ if (E.value != expected_hashes[E.key ]) {
552+ error_msgs.push_back (bullet + vformat (TTR (" File \" %s\" hash mismatch." ), E.key ));
553+ files_ok = false ;
554+ }
555+ }
556+ }
557+ for (const KeyValue<String, Vector<uint8_t >> &E : expected_hashes) {
558+ if (!file_hashes.has (E.key )) {
559+ error_msgs.push_back (bullet + vformat (TTR (" Missing file \" %s\" ." ), E.key ));
560+ files_ok = false ;
561+ }
562+ }
563+
564+ bool signature_ok = false ;
565+ if (!has_keys) {
566+ error_msgs.push_back (bullet + TTR (" Signature verification skipped, no trusted public keys configured." ));
567+ signature_ok = true ;
568+ }
569+ if (!signature_ok) {
570+ for (size_t i = 0 ; i < std::size (trusted_public_keys); i++) {
571+ Ref<CryptoKey> key = Ref<CryptoKey>(CryptoKey::create ());
572+ ERR_FAIL_COND (key.is_null ());
573+ if (key->load_from_string (trusted_public_keys[i], true ) != OK) {
574+ continue ;
575+ }
576+
577+ Ref<Crypto> crypto = Crypto::create ();
578+ ERR_FAIL_COND (crypto.is_null ());
579+ if (crypto->verify (HashingContext::HASH_SHA256, manifest_hash, signature, key)) {
580+ signature_ok = true ;
581+ break ;
582+ }
583+ }
584+ }
585+ if (!signature_ok) {
586+ for (const String &k : user_keys) {
587+ String ukey = vformat (" -----BEGIN PUBLIC KEY-----\n %s\n -----END PUBLIC KEY-----" , k);
588+ Ref<CryptoKey> key = Ref<CryptoKey>(CryptoKey::create ());
589+ ERR_FAIL_COND (key.is_null ());
590+ if (key->load_from_string (ukey, true ) != OK) {
591+ continue ;
592+ }
593+
594+ Ref<Crypto> crypto = Crypto::create ();
595+ ERR_FAIL_COND (crypto.is_null ());
596+ if (crypto->verify (HashingContext::HASH_SHA256, manifest_hash, signature, key)) {
597+ signature_ok = true ;
598+ break ;
599+ }
600+ }
601+ }
602+ if (!signature_ok) {
603+ error_msgs.push_back (bullet + TTR (" Signature verification failed." ));
604+ }
605+
606+ if (!files_ok || !signature_ok) {
607+ verification_fail_message_label->set_text (String (" \n " ).join (error_msgs));
608+ verification_dialog_fail->popup_centered ();
609+ _clenup (p_file, p_remove_file);
610+ return ;
611+ }
612+
613+ _install_file_selected (p_file, p_skip_progress);
614+ _clenup (p_file, p_remove_file);
615+ }
616+
617+ void ExportTemplateManager::_install_continue () {
618+ _install_file_selected (ver_file, ver_skip_progress);
619+ _clenup (ver_file, ver_remove_file);
620+ ver_file = String ();
621+ }
622+
623+ void ExportTemplateManager::_install_cancel () {
624+ _clenup (ver_file, ver_remove_file);
625+ ver_file = String ();
626+ }
627+
628+ void ExportTemplateManager::_install_file_selected (const String &p_file, bool p_skip_progress) {
629+ Ref<FileAccess> io_fa;
630+ zlib_filefunc_def io = zipio_create_io (&io_fa);
631+
632+ unzFile pkg = unzOpen2 (p_file.utf8 ().get_data (), &io);
633+ if (!pkg) {
634+ EditorNode::get_singleton ()->show_warning (TTR (" Can't open the export templates file." ));
635+ return ;
437636 }
438637 int ret = unzGoToFirstFile (pkg);
439638
@@ -476,7 +675,7 @@ bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_
476675 if (data_str.get_slice_count (" ." ) < 3 ) {
477676 EditorNode::get_singleton ()->show_warning (vformat (TTR (" Invalid version.txt format inside the export templates file: %s." ), data_str));
478677 unzClose (pkg);
479- return false ;
678+ return ;
480679 }
481680
482681 version = data_str;
@@ -493,7 +692,7 @@ bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_
493692 if (version.is_empty ()) {
494693 EditorNode::get_singleton ()->show_warning (TTR (" No version.txt found inside the export templates file." ));
495694 unzClose (pkg);
496- return false ;
695+ return ;
497696 }
498697
499698 Ref<DirAccess> d = DirAccess::create (DirAccess::ACCESS_FILESYSTEM);
@@ -502,7 +701,7 @@ bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_
502701 if (err != OK) {
503702 EditorNode::get_singleton ()->show_warning (TTR (" Error creating path for extracting templates:" ) + " \n " + template_path);
504703 unzClose (pkg);
505- return false ;
704+ return ;
506705 }
507706
508707 EditorProgress *p = nullptr ;
@@ -594,7 +793,6 @@ bool ExportTemplateManager::_install_file_selected(const String &p_file, bool p_
594793
595794 _update_template_status ();
596795 EditorSettings::get_singleton ()->set (" _export_template_download_directory" , p_file.get_base_dir ());
597- return true ;
598796}
599797
600798void ExportTemplateManager::_uninstall_template (const String &p_version) {
@@ -979,6 +1177,8 @@ ExportTemplateManager::ExportTemplateManager() {
9791177 set_hide_on_ok (false );
9801178 set_ok_button_text (TTR (" Close" ));
9811179
1180+ EDITOR_DEF_BASIC (" export/template_trusted_public_keys" , Vector<String>());
1181+
9821182 VBoxContainer *main_vb = memnew (VBoxContainer);
9831183 add_child (main_vb);
9841184
@@ -1162,11 +1362,56 @@ ExportTemplateManager::ExportTemplateManager() {
11621362 install_file_dialog->set_file_mode (FileDialog::FILE_MODE_OPEN_FILE);
11631363 install_file_dialog->set_current_dir (EDITOR_DEF (" _export_template_download_directory" , " " ));
11641364 install_file_dialog->add_filter (" *.tpz" , TTR (" Godot Export Templates" ));
1165- install_file_dialog->connect (" file_selected" , callable_mp (this , &ExportTemplateManager::_install_file_selected ).bind (false ) );
1365+ install_file_dialog->connect (" file_selected" , callable_mp (this , &ExportTemplateManager::_verify_and_install_file_selected ).bind (false , false ), CONNECT_DEFERRED );
11661366 add_child (install_file_dialog);
11671367
11681368 hide_dialog_accept = memnew (AcceptDialog);
11691369 hide_dialog_accept->set_text (TTR (" The templates will continue to download.\n You may experience a short editor freeze when they finish." ));
11701370 add_child (hide_dialog_accept);
11711371 hide_dialog_accept->connect (SceneStringName (confirmed), callable_mp (this , &ExportTemplateManager::_hide_dialog));
1372+
1373+ verification_dialog_accept = memnew (AcceptDialog);
1374+ verification_dialog_accept->add_cancel_button ();
1375+ verification_dialog_accept->set_title (TTR (" Unsigned Export Templates File" ));
1376+ verification_dialog_accept->set_ok_button_text (TTR (" Install Anyway" ));
1377+ add_child (verification_dialog_accept);
1378+ verification_dialog_accept->connect (SceneStringName (confirmed), callable_mp (this , &ExportTemplateManager::_install_continue));
1379+ verification_dialog_accept->connect (SNAME (" canceled" ), callable_mp (this , &ExportTemplateManager::_install_cancel));
1380+
1381+ VBoxContainer *verification_accept_message_vbox = memnew (VBoxContainer);
1382+ verification_dialog_accept->add_child (verification_accept_message_vbox);
1383+
1384+ verification_accept_message_title = memnew (Label);
1385+ verification_accept_message_title->set_text (TTR (" Can't validate the export templates file, templates file is not signed." ));
1386+ verification_accept_message_title->set_v_size_flags (Control::SIZE_SHRINK_BEGIN);
1387+ verification_accept_message_title->set_theme_type_variation (" HeaderSmall" );
1388+ verification_accept_message_vbox->add_child (verification_accept_message_title);
1389+
1390+ verification_accept_message_vbox->add_spacer ();
1391+
1392+ Label *verification_accept_message_label = memnew (Label);
1393+ verification_accept_message_label->set_v_size_flags (Control::SIZE_SHRINK_BEGIN);
1394+ verification_accept_message_label->set_text (TTR (" Installing this export templates file might put your computer at risk." ));
1395+ verification_accept_message_vbox->add_child (verification_accept_message_label);
1396+
1397+ verification_dialog_fail = memnew (AcceptDialog);
1398+ verification_dialog_fail->set_title (TTR (" Export Templates File Verification Failed" ));
1399+ add_child (verification_dialog_fail);
1400+
1401+ VBoxContainer *verification_fail_message_vbox = memnew (VBoxContainer);
1402+ verification_dialog_fail->add_child (verification_fail_message_vbox);
1403+
1404+ verification_fail_message_title = memnew (Label);
1405+ verification_fail_message_title->set_text (TTR (" Can't install the export templates file, templates file damaged or has invalid signature." ));
1406+ verification_fail_message_title->set_v_size_flags (Control::SIZE_SHRINK_BEGIN);
1407+ verification_fail_message_title->set_theme_type_variation (" HeaderSmall" );
1408+ verification_fail_message_vbox->add_child (verification_fail_message_title);
1409+
1410+ verification_fail_message_vbox->add_spacer ();
1411+
1412+ verification_fail_message_label = memnew (RichTextLabel);
1413+ verification_fail_message_label->set_focus_mode (Control::FOCUS_ACCESSIBILITY);
1414+ verification_fail_message_label->set_v_size_flags (Control::SIZE_EXPAND_FILL);
1415+ verification_fail_message_label->set_custom_minimum_size (Size2 (0 , 200 ) * EditorScale::get_scale ());
1416+ verification_fail_message_vbox->add_child (verification_fail_message_label);
11721417}
0 commit comments