From 105742acbc76758200a638dec61e0f6c1f396ca9 Mon Sep 17 00:00:00 2001 From: zxkmm <24917424+zxkmm@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:13:55 +0800 Subject: [PATCH] add random ext app (#2273) * copy paste from afsk * add generate thing * todo: remove uneeded code * todo: remove uneeded code * todo: asycnmsg detect indicate, check way to not use global password * removed audio * add log warn modal * small tune * remove drunk code * password var global * seed as text instead of console * remove console * should be almost done * naming fix * bitmap now moved to seperate folder, that header i deleted isn't related to firmware * get cmake format back * get cmake format back - try2 * try to revert cmake file * get cmake format back - try3 * get cmake format back - try4 * move to util * disable amp when launch * refactor name * cmake fix * try to revert cmake file * init in methods local var * user another methods to generate * change pause to flood * fix log * fine tune * clang format * fix name --- firmware/application/external/external.cmake | 5 + firmware/application/external/external.ld | 7 +- .../external/random_password/main.cpp | 82 ++++ .../random_password/ui_random_password.cpp | 371 ++++++++++++++++++ .../random_password/ui_random_password.hpp | 219 +++++++++++ firmware/graphics/key.png | Bin 0 -> 5218 bytes 6 files changed, 683 insertions(+), 1 deletion(-) create mode 100644 firmware/application/external/random_password/main.cpp create mode 100644 firmware/application/external/random_password/ui_random_password.cpp create mode 100644 firmware/application/external/random_password/ui_random_password.hpp create mode 100644 firmware/graphics/key.png diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 8bc794ec..cf0b6b8e 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -96,6 +96,10 @@ set(EXTCPPSRC #sstvtx external/sstvtx/main.cpp external/sstvtx/ui_sstvtx.cpp + + #random + external/random_password/main.cpp + external/random_password/ui_random_password.cpp ) set(EXTAPPLIST @@ -122,4 +126,5 @@ set(EXTAPPLIST adsbtx morse_tx sstvtx + random_password ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 932c76a6..f93a617e 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -46,6 +46,7 @@ MEMORY ram_external_app_adsbtx(rwx) : org = 0xADC50000, len = 32k ram_external_app_morse_tx(rwx) : org = 0xADC60000, len = 32k ram_external_app_sstvtx(rwx) : org = 0xADC70000, len = 32k + ram_external_app_random_password(rwx) : org = 0xADC80000, len = 32k } SECTIONS @@ -190,6 +191,10 @@ SECTIONS *(*ui*external_app*sstvtx*); } > ram_external_app_sstvtx - + .external_app_random_password : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_random_password.application_information)); + *(*ui*external_app*random_password*); + } > ram_external_app_random_password } diff --git a/firmware/application/external/random_password/main.cpp b/firmware/application/external/random_password/main.cpp new file mode 100644 index 00000000..049eb25b --- /dev/null +++ b/firmware/application/external/random_password/main.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 Bernd Herzog + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_random_password.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::random_password { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::random_password + +extern "C" { + +__attribute__((section(".external_app.app_random_password.application_information"), used)) application_information_t _application_information_random_password = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::random_password::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "random passwd", + /*.bitmap_data = */ { + 0xC0, + 0x03, + 0xE0, + 0x07, + 0x30, + 0x0C, + 0x30, + 0x0C, + 0x30, + 0x0C, + 0x30, + 0x0C, + 0xE0, + 0x07, + 0xC0, + 0x03, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x01, + 0x80, + 0x07, + 0x80, + 0x03, + 0x80, + 0x07, + 0x80, + 0x01, + }, + /*.icon_color = */ ui::Color::yellow().v, + /*.menu_location = */ app_location_t::UTILITIES, + + /*.m4_app_tag = portapack::spi_flash::image_tag_afsk_rx */ {'P', 'A', 'F', 'R'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} diff --git a/firmware/application/external/random_password/ui_random_password.cpp b/firmware/application/external/random_password/ui_random_password.cpp new file mode 100644 index 00000000..cbff01bd --- /dev/null +++ b/firmware/application/external/random_password/ui_random_password.cpp @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2017 Furrtek + * copyleft zxkmm + * Copyright (C) 2024 HToToo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui_random_password.hpp" +#include "ui_modemsetup.hpp" + +#include "modems.hpp" +#include "rtc_time.hpp" +#include "baseband_api.hpp" +#include "string_format.hpp" +#include "portapack_persistent_memory.hpp" +#include "file_path.hpp" + +using namespace portapack; +using namespace modems; +using namespace ui; + +namespace ui::external_app::random_password { + +void RandomPasswordLogger::log_raw_data(const std::string& data) { + log_file.write_entry(data); +} + +void RandomPasswordView::focus() { + button_refresh.focus(); +} + +RandomPasswordView::RandomPasswordView(NavigationView& nav) + : nav_{nav} { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + + add_children({&rssi, + &channel, + &field_rf_amp, + &field_lna, + &field_vga, + &field_frequency, + &check_log, + &button_modem_setup, + &labels, + &text_generated_passwd, + &text_char_type_hints, + &check_digits, + &check_latin_lower, + &check_latin_upper, + &check_punctuation, + &check_show_seeds, + &check_auto_send, + &button_refresh, + &button_show_qr, + &button_flood, + &button_send, + &field_digits, + &check_allow_confusable_chars, + &text_seed, + &progressbar}); + + // no idea what's these, i copied from afsk rx app and they seems needed' + auto def_bell202 = &modem_defs[0]; + persistent_memory::set_modem_baudrate(def_bell202->baudrate); + serial_format_t serial_format; + serial_format.data_bits = 7; + serial_format.parity = EVEN; + serial_format.stop_bits = 1; + serial_format.bit_order = LSB_FIRST; + persistent_memory::set_serial_format(serial_format); + + progressbar.set_max(30); + + check_log.set_value(logging); + check_log.on_select = [this](Checkbox&, bool v) { + if (v) { + nav_.display_modal( + "Warning", + "Sure?\n" + "this will save all generated\n" + "password to sdcard\n" + "in plain text\n" + "those which generated before\n" + "you check me, will lost", + YESNO, + [this, v](bool c) { + if (c) { + logging = v; + } else { + check_log.set_value(false); + // this is needed to check back to false cuz when trigger by human, the check to true already happened + // this blocked interface so won't accidently saved even if user checked but selected no later here, + // but take care of here if in the future implemented ticking/auto/batch save etc + } + }); + } else { + logging = v; + } + }; + + button_modem_setup.on_select = [&nav](Button&) { // copied from afsk rx app + nav.push(); + }; + + check_digits.on_select = [this](Checkbox&, bool) { + this->new_password(); + }; + + check_latin_lower.on_select = [this](Checkbox&, bool) { + this->new_password(); + }; + + check_latin_upper.on_select = [this](Checkbox&, bool) { + this->new_password(); + }; + + check_punctuation.on_select = [this](Checkbox&, bool) { + this->new_password(); + }; + + check_allow_confusable_chars.on_select = [this](Checkbox&, bool) { + this->new_password(); + }; + + button_refresh.on_select = [this](Button&) { + this->set_random_freq(); + this->new_password(); + }; + + button_show_qr.on_select = [this, &nav](Button&) { + nav.push(password.data()); + }; + + button_flood.on_select = [this](Button&) { + if (flooding) { + flooding = false; + button_flood.set_text("flood"); + } else { + flooding = true; + button_flood.set_text("stop"); + } + }; + button_send.on_select = [this, &nav](Button&) { + portapack::async_tx_enabled = true; + UsbSerialAsyncmsg::asyncmsg(password); + portapack::async_tx_enabled = false; + }; + + field_digits.on_change = [this](int32_t) { + clean_buffer(); + this->new_password(); + }; + + /// v check defauly val init + check_digits.set_value(true); + check_latin_lower.set_value(true); + check_latin_upper.set_value(true); + check_punctuation.set_value(true); + check_show_seeds.set_value(true); + field_digits.set_value(16); + ///^ check defauly val init + + logger = std::make_unique(); + if (logger) + logger->append(logs_dir / u"random.TXT"); + + // Auto-configure modem for LCR RX (will be removed later), copied from afsk rx app + baseband::set_afsk(persistent_memory::modem_baudrate(), 8, 0, false); + + receiver_model.enable(); + receiver_model.set_rf_amp(false); + set_random_freq(); + new_password(); +} + +void RandomPasswordView::on_data(uint32_t value, bool is_data) { + if (is_data) { + seed = static_cast(value); + text_seed.set(to_string_dec_uint(check_show_seeds.value() ? seed : 0)); + + /// v feed deque + seeds_deque.push_back(value); + if (seeds_deque.size() > MAX_DIGITS) { + seeds_deque.pop_front(); + } + + ///^ feed deque + + progressbar.set_value(seeds_deque.size()); + + if (flooding && seeds_deque.size() >= MAX_DIGITS) { + new_password(); + } + + } else { + text_generated_passwd.set("Baudrate estimation: ~"); + text_char_type_hints.set(to_string_dec_uint(value)); + } +} + +void RandomPasswordView::clean_buffer() { + seeds_deque = {}; + char_deque = {""}; +} + +void RandomPasswordView::on_freqchg(int64_t freq) { + field_frequency.set_value(freq); +} + +void RandomPasswordView::set_random_freq() { + std::srand(LPC_RTC->CTIME0); + // this is only for seed to visit random freq, the radio is still real random + + auto random_freq = 100000000 + (std::rand() % 900000000); // 100mhz to 1ghz + receiver_model.set_target_frequency(random_freq); + field_frequency.set_value(random_freq); +} + +void RandomPasswordView::new_password() { + if (seeds_deque.size() < MAX_DIGITS) { + seeds_buffer_not_full = true; + text_generated_passwd.set("wait seeds buffer full"); + text_char_type_hints.set("then press generate"); + return; + } + password = ""; + std::string charset = ""; + std::string char_type_hints = ""; + + if (check_digits.value()) + charset += "0123456789"; + if (check_latin_lower.value()) + charset += "abcdefghijklmnopqrstuvwxyz"; + if (check_latin_upper.value()) + charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + if (check_punctuation.value()) + charset += ".,-!?"; + + if (!check_allow_confusable_chars.value()) { + charset.erase(std::remove_if(charset.begin(), charset.end(), + [](char c) { return c == '0' || c == 'O' || c == 'o' || c == '1' || c == 'l'; }), + charset.end()); + } + + if (charset.empty()) { + text_generated_passwd.set("generate failed,"); + text_char_type_hints.set("select at least 1 type"); + return; + } + + int password_length = field_digits.value(); + + /*the seeds_buffer were feed streaming by AFSK, + * and when generate, it use each seed for each char, and uint seeds totally can generate UINT_MAX result, + * which already cover the 10+26+25+4 (123+abc+abc+.!) + * so total possible password would be PW_LENGTH ^ (10+26+25+4), which already covered all the possible solution + * (assume AFSK data is averaged in chaotic space, which maybe no one can garentee but I hope so) + * */ + + for (int i = 0; i < password_length; i++) { + unsigned int seed = seeds_deque[i]; + std::srand(seed); + char c = charset[std::rand() % charset.length()]; + password += c; + char_deque.push_back(std::string(1, c)); + + if (std::isdigit(c)) { + char_type_hints += "1"; + } else if (std::islower(c)) { + char_type_hints += "a"; + } else if (std::isupper(c)) { + char_type_hints += "A"; + } else { + char_type_hints += ","; + } + } + + text_generated_passwd.set(password); + text_char_type_hints.set(char_type_hints); + + paint_password_hints(); // TODO: why flash and disappeared + + if (logger && logging) { + str_log += generate_log_line(); + logger->log_raw_data(str_log); + str_log = ""; + } + + if (check_auto_send.value() || flooding) { + portapack::async_tx_enabled = true; + // printing out seeds buufer + // for (auto seed : seeds_deque) { + // UsbSerialAsyncmsg::asyncmsg(std::to_string(seed)); + // } + UsbSerialAsyncmsg::asyncmsg(password); + portapack::async_tx_enabled = false; + } + + clean_buffer(); +} + +// TODO: why flash and disappeared +// tried: +// 1. paint inline in new_password func +// 2. paint in a seperate func and call from new_password +// 3. override nav's paint func (i think it can tried to capture same obj) and paint, hoping set_dirty handle it correctly +// 4. override nav's paint func (i think it can tried to capture same obj) and paint in a seperate func, hoping set_dirty handle it correctly +// all these methods failed, and all of them only flash and disappeared. only when set_dirty in on_data (which seems incorrect), and it keep flashing never stop. but see painted content (flashing too) +// btw this is not caused by the seed text set thing, cuz commented it out not helping. +void RandomPasswordView::paint_password_hints() { + Painter painter; + const int char_width = 8; + const int char_height = 16; + const int start_y = 6 * char_height + 5; + const int rect_height = 4; + + for (size_t i = 0; i < password.length(); i++) { + char c = password[i]; + Color color; + if (std::isdigit(c)) { + color = Color::red(); + } else if (std::islower(c)) { + color = Color::green(); + } else if (std::isupper(c)) { + color = Color::blue(); + } else { + color = Color::white(); + } + + painter.fill_rectangle( + {{static_cast(i) * char_width, start_y}, + {char_width, rect_height}}, + color); + } +} + +std::string RandomPasswordView::generate_log_line() { + std::string seeds_set = ""; + for (auto seed : seeds_deque) { + seeds_set += std::to_string(seed); + seeds_set += " "; + } + std::string line = "\npassword=" + password + + "\nseeds=" + seeds_set + + "\n"; + return line; +} + +RandomPasswordView::~RandomPasswordView() { + receiver_model.disable(); + baseband::shutdown(); +} + +} // namespace ui::external_app::random_password diff --git a/firmware/application/external/random_password/ui_random_password.hpp b/firmware/application/external/random_password/ui_random_password.hpp new file mode 100644 index 00000000..4f22bfe3 --- /dev/null +++ b/firmware/application/external/random_password/ui_random_password.hpp @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2017 Furrtek + * copyleft zxkmm + * Copyright (C) 2024 HToToo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_RANDOM_PASSWORD_H__ +#define __UI_RANDOM_PASSWORD_H__ + +#define MAX_DIGITS 30 + +#include "ui.hpp" +#include "ui_language.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_freq_field.hpp" +#include "ui_record_view.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "log_file.hpp" +#include "utility.hpp" +#include "ui_qrcode.hpp" +#include "usb_serial_asyncmsg.hpp" +#include + +using namespace ui; + +namespace ui::external_app::random_password { + +class RandomPasswordLogger { // TODO: log is broken after introduced the buffer thing + public: + Optional append(const std::filesystem::path& filename) { + return log_file.append(filename); + } + + void log_raw_data(const std::string& data); + + private: + LogFile log_file{}; +}; + +class RandomPasswordView : public View { + public: + RandomPasswordView(NavigationView& nav); + ~RandomPasswordView(); + + void focus() override; + + std::string title() const override { return "r.passwd"; }; + + private: + unsigned int seed = 0; // extern void srand (unsigned int __seed) __THROW; + std::string password = ""; + std::deque seeds_deque = {0}; + std::deque char_deque = {""}; + bool seeds_buffer_not_full = true; + bool in_benchmark = false; + bool flooding = false; + bool logging = false; + std::string str_log{""}; + + void on_data(uint32_t value, bool is_data); + void clean_buffer(); + void new_password(); + std::string generate_log_line(); + void paint_password_hints(); + + NavigationView& nav_; + RxRadioState radio_state_{}; + app_settings::SettingsManager settings_{ + "rx_afsk", app_settings::Mode::RX}; + + Labels labels{ + {{0 * 8, 0 * 16}, "------------seeds-------------", Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 3 * 16}, "-----------password-----------", Theme::getInstance()->fg_light->foreground}, + {{5 * 8, 7 * 16 - 2}, "digits:", Theme::getInstance()->fg_light->foreground}, + }; + + RFAmpField field_rf_amp{ + {13 * 8, 1 * 16}}; + LNAGainField field_lna{ + {15 * 8, 1 * 16}}; + VGAGainField field_vga{ + {18 * 8, 1 * 16}}; + + RSSI rssi{ + {21 * 8, 1 * 16 + 0, 6 * 8, 4}}; + Channel channel{ + {21 * 8, 1 * 16 + 5, 6 * 8, 4}}; + + RxFrequencyField field_frequency{ + {0 * 8, 1 * 16}, + nav_}; + + Button button_modem_setup{ + {screen_width - 12 * 8, 2 * 16 - 1, 96, 16 + 2}, + "AFSK modem"}; + + Text text_seed{ + {0, 2 * 16, 10 * 8, 16}, + "0000000000"}; + + ProgressBar progressbar{ + {10 * 8 + 2, 2 * 16, screen_width - 96 - (10 * 8 + 4) - 1, 16}}; + + Text text_generated_passwd{ + {0, 4 * 16, screen_width, 28}, + "000000000000000000000000000000"}; + + Text text_char_type_hints{ + {0, 5 * 16, screen_width, 28}, + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"}; + + Checkbox check_show_seeds{ + {17 * 8, 8 * 16}, + 6, + "show seed"}; + + Checkbox check_auto_send{ + {1 * 8, 8 * 16}, + 20, + "auto send"}; + + Checkbox check_punctuation{ + {17 * 8, 12 * 16}, + 6, + ".,-!?"}; + + Checkbox check_allow_confusable_chars{ + {1 * 8, 10 * 16}, + 20, + "0 O o 1 l"}; + + Checkbox check_digits{ + {1 * 8, 12 * 16}, + 3, + "123"}; + + Checkbox check_latin_lower{ + {1 * 8, 14 * 16}, + 3, + "abc"}; + + Checkbox check_latin_upper{ + {17 * 8, 14 * 16}, + 3, + "ABC"}; + + Checkbox check_log{ + {17 * 8, 10 * 16}, + 3, + "savin"}; + + Button button_flood{ + {0 * 8, 15 * 16 + 18, screen_width / 2, 22}, + "flood"}; + + Button button_send{ + {screen_width / 2 + 2, 15 * 16 + 18, screen_width / 2 - 2, 22}, + "send pwd"}; + + Button button_refresh{ + {0 * 8, 17 * 16 + 10, screen_width / 2, 22}, + "generate"}; + + Button button_show_qr{ + {screen_width / 2 + 2, 17 * 16 + 10, screen_width / 2 - 2, 22}, + "show QR"}; + + NumberField field_digits{ + {16 * 8, 7 * 16 - 2}, + 2, + {1, 30}, + 1, + ' '}; + + void on_data_afsk(const AFSKDataMessage& message); + + std::unique_ptr logger{}; + + MessageHandlerRegistration message_handler_packet{ + Message::ID::AFSKData, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_data(message->value, message->is_data); + }}; + + MessageHandlerRegistration message_handler_freqchg{ + Message::ID::FreqChangeCommand, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_freqchg(message->freq); + }}; + + void on_freqchg(int64_t freq); + void set_random_freq(); +}; + +} // namespace ui::external_app::random_password + +#endif /*__UI_RANDOM_PASSWORD_H__*/ diff --git a/firmware/graphics/key.png b/firmware/graphics/key.png new file mode 100644 index 0000000000000000000000000000000000000000..4328e0d9f741b9e1ce50779f4bd7085e639acb6e GIT binary patch literal 5218 zcmeHLdu$VR9KUXkjlsr%ZM@^93p&8-J$ilIWn~#_VMRJBEs8h{*Sp`9gY`l0wsn)C zVThuO_<*PgB1V)Lz{ljFC=)OOCO%Pt_?kfxHHv}(6(fG{+I3qbF-zbdvo^o}Uf=KM z_x*mp-`}^ruJ+f?D99h1k6~DWx5hmO`XasMWy5DVF)RSx2WXyG_If8{c`yM<(>0h$ zmw{&LHO?|+W4hKVXd_^(%jj*aDaYvBVLUE9UJK*$w2ZDpo3gM1=r=%PU|bCS^U#!g zQfKXhvHr?4-iJU%m+0I8j?VR?$<83#_=d4}UD8qPO8)nk#4RQFf( z2-vq$sjl{WtE=&NA|@;K2*Z*aS|4)N9P}68Jv*>v?&QM8qK3o%joaGpbACFxc=NQa zCG#7o=I^g%m74i=<8zL!Xsf(`Ztke?f(dmwRlnrEbz;BwL(k0D$w{x=`|JL1TQ^*I z@9TXRvCTKh>pv;UX5U!+?1-iGBMVBhUpT#=S`i${bhLBcnFrL?FAg5Mu($mEv->W5 z_d`G}&0c0++CD~JmuySQ9gE{%USHVp^tKV3My`3{WBk{$VG$|jS_1vk| z2j2>9Zd+F_O_)fRw?z)Lo;)bWP3SK^$d#UZKj-6)%k3{E`7iyiy&mSr; zI-aw>;j_P|?)vG};I2Cd?f7k1=SP@a>3w1M&cnk$=q#wK+igR`_Iz#{v}x#xwG)oa zp7_nVH!qH94U~@FlhbnS^H+DixAaf!8Rh09F0+Bl!&qM?&A1_= zEpeIWK^|4d5(sB4tc4;xNu`l7PtC_G5|Yf%aZgV}z|3V{q-k-UB%7L=EKOEREDjYB^JeXOd%9&&|GFS?Bm_`MdChR20p5$ zSpYuBq!1@*3q?jEWN!^s^E3iT+Mvs7sKMqqBIh7A){qd9rx8W9vfdDqn9+|nBSLR}W4P1X5KzzH_L1)P-4F&T zpO1IP#0K3wuiItT&*!C>s7Smq6rD213XGkwg&ZQm(oRGONR$b|MzONh;iT+rs27ws zs%k=1L^>3JTNJ>NCB)I9L=!ffY$I5S0|*IXYj@fl6tZ%nEYrOpW+xQLN};~DRXUUe zP$*<&?TB*{cAFinVL6H5oUA~wNS3Wun<&zPWI#zGKRuR+2oO#sB7_kckA{s6op8R& z?{%3O3)QpYuNO2K6kO(6O0*%_vk_DxD4+>Co3w**u#C;Y(yWzZ9gZHMn^8iAOw=)r z+G*_QVd23UAXd=x6ab8M@P)5VAVG^Ig0Wb=%bcmYnQb3jCrQu*x1b>a&D7ywcO9NW zIe98&z7FS$Ns8S3U()*J!7D%>LSCb&aDKC~l)6y?v@|tKP3sloD&e?sDR@Clxu6P- zD8(>frMARHLNtt^dZg2}dtJFqDlm-1*;&R(a1Of@q9%xh;IvT$YYlM@2P1|APBd7^ zpsO)iYZ4NuDhxaVSCF2@HxyoO1Zq-W>83?Uj{*=zP#nQH48rWZl}-~zUVRtA52q|U z?1V&fx?^^MaMB@`unTqvC$f|*+E8DkuYimCcXMACD@eU8b_cbB{8!jh0=?=eP?K7N z`V8fs%+&idXAqLB`AVnT)jR^n2M*~MzXNm)(A6&n`XxM&T?2IWi-CR#4`kOBqbtAX zi4jHNhh7sr4;H;Mwh6G)kQ zj_z8vH7|SR8vIDfn^f`9`H!{C%<)Hr1EC?S^2@WaGjmOcE%>`zKoRqLYTY}gEnM*r Dug2NR literal 0 HcmV?d00001