pax_global_header00006660000000000000000000000064145211202040014501gustar00rootroot0000000000000052 comment=29c501716579e564526944876618e185cd8ede17 OTPClient-3.2.1/000077500000000000000000000000001452112020400133055ustar00rootroot00000000000000OTPClient-3.2.1/.ci/000077500000000000000000000000001452112020400137565ustar00rootroot00000000000000OTPClient-3.2.1/.ci/install_deps.sh000066400000000000000000000005711452112020400167760ustar00rootroot00000000000000#!/bin/bash set -e __compile_and_install() { cmake .. -DCMAKE_INSTALL_PREFIX=/usr make -j2 make install } git clone https://github.com/paolostivanin/libbaseencode.git cd libbaseencode && mkdir build && cd "$_" __compile_and_install cd ../.. git clone https://github.com/paolostivanin/libcotp.git cd libcotp && mkdir build && cd "$_" __compile_and_install cd ../.. OTPClient-3.2.1/.ci/install_otpclient.sh000066400000000000000000000002401452112020400200350ustar00rootroot00000000000000#!/bin/bash set -e __compile_and_install() { cmake .. -DCMAKE_INSTALL_PREFIX=/usr make -j2 make install } mkdir build && cd "$_" __compile_and_install OTPClient-3.2.1/.circleci/000077500000000000000000000000001452112020400151405ustar00rootroot00000000000000OTPClient-3.2.1/.circleci/config.yml000066400000000000000000000046171452112020400171400ustar00rootroot00000000000000version: 2.0 jobs: ubuntu2004: docker: - image: ubuntu:20.04 steps: - checkout - run: apt update && DEBIAN_FRONTEND=noninteractive apt -y install git gcc clang cmake libgcrypt20-dev libgtk-3-dev libzip-dev libjansson-dev libpng-dev libzbar-dev libprotobuf-c-dev libsecret-1-dev uuid-dev libprotobuf-dev libqrencode-dev - run: chmod +x .ci/install_deps.sh && .ci/install_deps.sh - run: chmod +x .ci/install_otpclient.sh && .ci/install_otpclient.sh ubuntuLatestRolling: docker: - image: ubuntu:rolling steps: - checkout - run: apt update && DEBIAN_FRONTEND=noninteractive apt -y install git gcc clang cmake libgcrypt20-dev libgtk-3-dev libzip-dev libjansson-dev libpng-dev libzbar-dev libprotobuf-c-dev libsecret-1-dev uuid-dev libprotobuf-dev libqrencode-dev - run: chmod +x .ci/install_deps.sh && .ci/install_deps.sh - run: chmod +x .ci/install_otpclient.sh && .ci/install_otpclient.sh debianLatestStable: docker: - image: debian:latest steps: - checkout - run: apt update && apt -y install git gcc clang cmake libgcrypt20-dev libgtk-3-dev libzip-dev libjansson-dev libpng-dev libzbar-dev libprotobuf-c-dev libsecret-1-dev uuid-dev libprotobuf-dev libqrencode-dev - run: chmod +x .ci/install_deps.sh && .ci/install_deps.sh - run: chmod +x .ci/install_otpclient.sh && .ci/install_otpclient.sh fedoraLatestStable: docker: - image: fedora:latest steps: - checkout - run: dnf -y update && dnf -y install git gcc clang cmake make libgcrypt-devel gtk3-devel libzip-devel jansson-devel libpng-devel zbar-devel protobuf-c-devel libsecret-devel libuuid-devel protobuf-devel qrencode-devel - run: chmod +x .ci/install_deps.sh && .ci/install_deps.sh - run: chmod +x .ci/install_otpclient.sh && .ci/install_otpclient.sh archlinux: docker: - image: archlinux:latest steps: - checkout - run: pacman -Syu --noconfirm && pacman -S --noconfirm pkg-config git gtk3 libgcrypt zbar gcc clang cmake make libzip jansson libpng protobuf-c libsecret util-linux-libs qrencode - run: chmod +x .ci/install_deps.sh && .ci/install_deps.sh - run: chmod +x .ci/install_otpclient.sh && .ci/install_otpclient.sh workflows: version: 2 build: jobs: - ubuntu2004 - ubuntuLatestRolling - debianLatestStable - fedoraLatestStable - archlinux OTPClient-3.2.1/.github/000077500000000000000000000000001452112020400146455ustar00rootroot00000000000000OTPClient-3.2.1/.github/workflows/000077500000000000000000000000001452112020400167025ustar00rootroot00000000000000OTPClient-3.2.1/.github/workflows/codeql-analysis.yml000066400000000000000000000022121452112020400225120ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '30 13 * * 1' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: [ 'cpp' ] steps: - name: Checkout repository uses: actions/checkout@v2 - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} - name: Install Dependencies run: | sudo apt update && DEBIAN_FRONTEND=noninteractive sudo apt -y install git gcc clang cmake libgcrypt20-dev libgtk-3-dev libzip-dev libjansson-dev libpng-dev libzbar-dev libprotobuf-c-dev libsecret-1-dev uuid-dev libprotobuf-dev libqrencode-dev git clone https://github.com/paolostivanin/OTPClient ./OTPClient cd OTPClient && chmod +x .ci/install_deps.sh && sudo .ci/install_deps.sh - name: Build run: | mkdir build && cd $_ cmake .. make - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 OTPClient-3.2.1/.gitignore000066400000000000000000000005551452112020400153020ustar00rootroot00000000000000cmake-build-debug/ build/ tests/ .flatpak-builder/ flatpak/repo/ TODO* .idea/ .PVS-Studio/ # Object files *.o *.ko *.obj *.elf # Precompiled Headers *.gch *.pch # Libraries *.lib *.a *.la *.lo # Shared objects (inc. Windows DLLs) *.dll *.so *.so.* *.dylib # Executables *.exe *.out *.app *.i*86 *.x86_64 *.hex # Debug files *.dSYM/ # Backup files *.ui~ *.c~ OTPClient-3.2.1/CMakeLists.txt000066400000000000000000000224601452112020400160510ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.16) project(OTPClient VERSION "3.2.1" LANGUAGES "C") include(GNUInstallDirs) configure_file("src/common/version.h.in" "version.h") set (GETTEXT_PACKAGE ${CMAKE_PROJECT_NAME}) include_directories(${PROJECT_BINARY_DIR}) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) option(USE_FLATPAK_APP_FOLDER "Use flatpak app's config folder to store the database" OFF) option(BUILD_GUI "Build the GUI" ON) option(BUILD_CLI "Build the CLI" ON) set(CMAKE_C_STANDARD 11) set(CMAKE_C_FLAGS "-Wall -Wextra -O2 -Wformat=2 -Wmissing-format-attribute -fstack-protector-strong -Wundef -Wmissing-format-attribute") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fdiagnostics-color=always -Wstrict-prototypes -Wunreachable-code") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wchar-subscripts -Wwrite-strings -Wpointer-arith -Wbad-function-cast -Wcast-align") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=format-security -Werror=implicit-function-declaration -Wno-sign-compare") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2") if(CMAKE_COMPILER_IS_GNUCC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pie -fPIE") endif() if(USE_FLATPAK_APP_FOLDER) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_FLATPAK_APP_FOLDER") endif() if(BUILD_GUI) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DBUILD_GUI") endif() if(BUILD_CLI) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DBUILD_CLI") endif() add_definitions(-DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\") if(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--no-add-needed -Wl,--as-needed -Wl,--no-undefined") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,relro,-z,now") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-add-needed -Wl,--as-needed") set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,-z,relro,-z,now") endif() find_package(PkgConfig REQUIRED) find_package(Protobuf 3.6.0 REQUIRED) find_package(Gcrypt 1.8.0 REQUIRED) pkg_check_modules(COTP REQUIRED cotp>=1.2.8) if(${COTP_VERSION} LESS 2.0.0) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCOTP_OLD_LIB") pkg_check_modules(BASEENCODE REQUIRED baseencode>=1.0.15) endif() pkg_check_modules(PNG REQUIRED libpng>=1.6.30) pkg_check_modules(JANSSON REQUIRED jansson>=2.12) pkg_check_modules(ZBAR REQUIRED zbar>=0.20) pkg_check_modules(GTK3 REQUIRED gtk+-3.0>=3.24.0) pkg_check_modules(GLIB2 REQUIRED glib-2.0>=2.64.0) pkg_check_modules(GIO REQUIRED gio-2.0>=2.64.0) pkg_check_modules(UUID REQUIRED uuid>=2.34.0) pkg_check_modules(PROTOC REQUIRED libprotobuf-c>=1.3.0) pkg_check_modules(LIBSECRET REQUIRED libsecret-1>=0.20.0) pkg_check_modules(LIBQRENCODE REQUIRED libqrencode>=4.0.2) set(GUI_HEADER_FILES src/common/common.h src/add-common.h src/gui-common.h src/data.h src/db-misc.h src/file-size.h src/get-builder.h src/gquarks.h src/imports.h src/liststore-misc.h src/manual-add-cb.h src/message-dialogs.h src/otpclient.h src/parse-uri.h src/password-cb.h src/qrcode-parser.h src/treeview.h src/common/exports.h src/lock-app.h src/common/get-providers-data.h src/change-db-cb.h src/new-db-cb.h src/db-actions.h src/google-migration.pb-c.h src/secret-schema.h src/change-pwd-cb.h src/settings-cb.h src/shortcuts-cb.h src/webcam-add-cb.h src/edit-row-cb.h src/show-qr-cb.h src/dbinfo-cb.h) set(GUI_SOURCE_FILES src/common/common.c src/add-common.c src/common/andotp.c src/app.c src/gui-common.c src/db-misc.c src/edit-row-cb.c src/file-size.c src/get-builder.c src/gquarks.c src/imports.c src/liststore-misc.c src/main.c src/manual-add-cb.c src/message-dialogs.c src/parse-data.c src/parse-uri.c src/password-cb.c src/qrcode-parser.c src/add-from-qr.c src/settings-cb.c src/shortcuts-cb.c src/treeview.c src/webcam-add-cb.c src/exports.c src/lock-app.c src/common/freeotp.c src/common/aegis.c src/change-db-cb.c src/new-db-cb.c src/db-actions.c src/change-file-cb.c src/google-migration.pb-c.c src/secret-schema.c src/about_diag_cb.c src/show-qr-cb.c src/setup-signals-shortcuts.c src/change-pwd-cb.c src/dbinfo-cb.c) set(CLI_HEADER_FILES src/cli/help.h src/cli/get-data.h src/common/common.h src/db-misc.h src/gquarks.h src/file-size.h src/common/exports.h src/parse-uri.h src/common/get-providers-data.h src/google-migration.pb-c.h src/secret-schema.h) set(CLI_SOURCE_FILES src/cli/main.c src/cli/help.c src/cli/get-data.c src/common/common.c src/db-misc.c src/gquarks.c src/file-size.c src/parse-uri.c src/common/andotp.c src/common/aegis.c src/common/freeotp.c src/secret-schema.c src/google-migration.pb-c.c) if(BUILD_GUI AND BUILD_CLI) list(APPEND CLI_SOURCE_FILES src/treeview.c src/liststore-misc.c src/gui-common.c src/add-common.c src/imports.c src/password-cb.c src/get-builder.c src/message-dialogs.c) list(APPEND CLI_HEADER_FILES src/treeview.h src/liststore-misc.h src/gui-common.h src/add-common.h src/imports.h src/password-cb.h src/get-builder.h src/message-dialogs.h) endif() if(BUILD_GUI) include_directories(${GTK3_INCLUDE_DIRS} ${GCRYPT_INCLUDE_DIRS} ${COTP_INCLUDE_DIRS} ${BASEENCODE_INCLUDE_DIRS} ${LIBZIP_INCLUDE_DIRS} ${PNG_INCLUDE_DIRS} ${JANSSON_INCLUDE_DIRS} ${ZBAR_INCLUDE_DIRS} ${UUID_INCLUDE_DIRS} ${PROTOC_INCLUDE_DIRS} ${LIBSECRET_INCLUDE_DIRS} ${LIBQRENCODE_INCLUDE_DIRS}) add_executable(${PROJECT_NAME} ${GUI_SOURCE_FILES} ${GUI_HEADER_FILES}) target_link_libraries(${PROJECT_NAME} ${GTK3_LIBRARIES} ${GCRYPT_LIBRARIES} ${COTP_LIBRARIES} ${BASEENCODE_LIBRARIES} ${LIBZIP_LIBRARIES} ${PNG_LIBRARIES} ${JANSSON_LIBRARIES} ${ZBAR_LIBRARIES} ${UUID_LIBRARIES} ${PROTOC_LIBRARIES} ${LIBSECRET_LIBRARIES} ${LIBQRENCODE_LIBRARIES}) set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "otpclient") install(TARGETS ${PROJECT_NAME} DESTINATION bin) endif() if(BUILD_CLI) include_directories(${GTK3_INCLUDE_DIRS} ${GCRYPT_INCLUDE_DIRS} ${BASEENCODE_INCLUDE_DIRS} ${COTP_INCLUDE_DIRS} ${JANSSON_INCLUDE_DIRS} ${UUID_INCLUDE_DIRS} ${PROTOC_INCLUDE_DIRS} ${LIBSECRET_INCLUDE_DIRS}) if(BUILD_GUI) include_directories( ${LIBZIP_INCLUDE_DIRS} ${PNG_INCLUDE_DIRS} ${ZBAR_INCLUDE_DIRS} ${LIBQRENCODE_INCLUDE_DIRS}) endif() add_executable(${PROJECT_NAME}-cli ${CLI_SOURCE_FILES} ${CLI_HEADER_FILES}) target_link_libraries(${PROJECT_NAME}-cli ${GLIB2_LIBRARIES} ${GIO_LIBRARIES} ${GCRYPT_LIBRARIES} ${BASEENCODE_LIBRARIES} ${COTP_LIBRARIES} ${JANSSON_LIBRARIES} ${UUID_LIBRARIES} ${LIBSECRET_LIBRARIES} ${PROTOC_LIBRARIES}) if(BUILD_GUI) target_link_libraries(${PROJECT_NAME}-cli ${GTK3_LIBRARIES} ${LIBZIP_LIBRARIES} ${PNG_LIBRARIES} ${ZBAR_LIBRARIES} ${PROTOC_LIBRARIES} ${LIBSECRET_LIBRARIES} ${UUID_LIBRARIES} ${LIBQRENCODE_LIBRARIES}) endif() set_target_properties(${PROJECT_NAME}-cli PROPERTIES OUTPUT_NAME "otpclient-cli") install(TARGETS ${PROJECT_NAME}-cli DESTINATION bin) endif() install(FILES data/com.github.paolostivanin.OTPClient.desktop DESTINATION share/applications) install(FILES data/com.github.paolostivanin.OTPClient.appdata.xml DESTINATION share/metainfo) install(FILES src/ui/otpclient.ui DESTINATION share/otpclient) install(FILES src/ui/shortcuts.ui DESTINATION share/otpclient) install(FILES man/otpclient.1.gz DESTINATION share/man/man1) install(FILES man/otpclient-cli.1.gz DESTINATION share/man/man1) install(FILES data/icons/com.github.paolostivanin.OTPClient.svg DESTINATION share/icons/hicolor/scalable/apps) install(FILES data/icons/com.github.paolostivanin.OTPClient-symbolic.svg DESTINATION share/icons/hicolor/scalable/apps) OTPClient-3.2.1/LICENSE000066400000000000000000001045151452112020400143200ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 3 of the License, 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. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . OTPClient-3.2.1/README.md000066400000000000000000000100531452112020400145630ustar00rootroot00000000000000# OTPClient CircleCI Coverity Scan Build Status Highly secure and easy to use GTK+ software for two-factor authentication that supports both Time-based One-time Passwords (TOTP) and HMAC-Based One-Time Passwords (HOTP). ## Requirements | Name | Min Version | |----------------------------------------------------|-------------| | GTK+ | 3.24 | | Glib | 2.64.0 | | jansson | 2.12 | | libgcrypt | 1.8.0 | | libpng | 1.6.30 | | [libcotp](https://github.com/paolostivanin/libcotp) | 2.0.0 | | zbar | 0.20 | | protobuf-c | 1.3.0 | | protobuf | 3.6.0 | | uuid | 2.34 | | libsecret | 0.20 | | qrencode | 4.0.2 | :warning: Please note that the memlock value should be `>= 4 MB`. Any value less than this may cause issues when dealing with tens of tokens (especially when importing from third parties backups). See this [wiki section](https://github.com/paolostivanin/OTPClient/wiki/Secure-Memory-Limitations) for info on how to check the current value and set, if needed, a higher one. ## Features - integration with the OS' secret service provider via libsecret - support both TOTP and HOTP - support setting custom digits (between 4 and 10 inclusive) - support setting a custom period (between 10 and 120 seconds inclusive) - support SHA1, SHA256 and SHA512 algorithms - support for Steam codes (please read [THIS PAGE](https://github.com/paolostivanin/OTPClient/wiki/Steam-Support)) - import and export encrypted/plain [andOTP](https://github.com/flocke/andOTP) backup - import and export encrypted/plain [Aegis](https://github.com/beemdevelopment/Aegis) backup - import and export plain [FreeOTPPlus](https://github.com/helloworld1/FreeOTPPlus) backup (key URI format only) - import of Google's migration QR codes - local database is encrypted using AES256-GCM - key is derived using PBKDF2 with SHA512 and 100k iterations - decrypted file is never saved (and hopefully never swapped) to disk. While the app is running, the decrypted content resides in a "secure memory" buffer allocated by Gcrypt ## Testing * Before each release, I run PVS Studio and Coverity in order to catch even more bugs. * With every commit to master, OTPClient is compiled in CircleCI against different distros ## Protobuf The protobuf files needed to decode Google's otpauth-migration qr codes have been generated with `protoc --c_out=src/ proto/google-migration.proto` ## Wiki For things like roadmap, screenshots, how to use OTPClient, etc, please have a look at the [project's wiki](https://github.com/paolostivanin/OTPClient/wiki). You'll find a lot of useful information there. ## Manual installation If OTPClient hasn't been packaged for your distro ([check here](https://github.com/paolostivanin/OTPClient/wiki/Tested-OS-&-Packages#packages)) and your distro doesn't support Flatpak, then you'll have to manually compile and install OTPClient. 1. install all the needed libraries listed under [requirements](#requirements) 2. clone and install OTPClient: ``` git clone https://github.com/paolostivanin/OTPClient.git cd OTPClient mkdir build && cd build cmake -DCMAKE_INSTALL_PREFIX=/usr .. make sudo make install ``` ## License This software is released under the GPLv3 license. Please have a look at the [LICENSE](LICENSE) file for more details. OTPClient-3.2.1/SECURITY.md000066400000000000000000000024641452112020400151040ustar00rootroot00000000000000# Security Policy ## Supported Versions The following list describes whether a version is eligible or not for security updates. | Version | Supported | EOL | |---------|--------------------|-------------| | 3.2.x | :white_check_mark: | - | | 3.1.x | :white_check_mark: | 30-Nov-2023 | | 3.0.x | :x: | 31-Dec-2022 | | 2.6.x | :x: | 15-Jan-2023 | | 2.5.x | :x: | 31-Aug-2022 | | 2.4.x | :x: | 15-May-2022 | | 2.3.x | :x: | 28-Feb-2021 | | 2.2.x | :x: | 27-May-2020 | | 2.1.x | :x: | 20-Apr-2020 | | 2.0.x | :x: | 08-Mar-2020 | | 1.5.x | :x: | 31-Mar-2020 | | < 1.5.0 | :x: | 27-Jun-2019 | ## Reporting a Vulnerability In case you should find a vulnerability, please report it privately to me via [e-mail](mailto:info@paolostivanin.com). The following is the workflow: - security issue is found, an e-mail is sent to me - within 24 hours I will reply to your e-mail with some info like, for example, whether it actually is a security issue and how serious it is - within 7 days I will develop and ship a fix - once the update is out I will open a [security advisory](https://github.com/paolostivanin/OTPClient/security/advisories) OTPClient-3.2.1/cmake/000077500000000000000000000000001452112020400143655ustar00rootroot00000000000000OTPClient-3.2.1/cmake/FindGcrypt.cmake000066400000000000000000000030401452112020400174350ustar00rootroot00000000000000# Copyright (C) 2011 Felix Geyer # # 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) # version 3 of the License. # # 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. If not, see . find_path(GCRYPT_INCLUDE_DIR gcrypt.h) find_library(GCRYPT_LIBRARIES gcrypt) mark_as_advanced(GCRYPT_LIBRARIES GCRYPT_INCLUDE_DIR) if(GCRYPT_INCLUDE_DIR AND EXISTS "${GCRYPT_INCLUDE_DIR}/gcrypt.h") file(STRINGS "${GCRYPT_INCLUDE_DIR}/gcrypt.h" GCRYPT_H REGEX "^#define GCRYPT_VERSION \"[^\"]*\"$") string(REGEX REPLACE "^.*GCRYPT_VERSION \"([0-9]+).*$" "\\1" GCRYPT_VERSION_MAJOR "${GCRYPT_H}") string(REGEX REPLACE "^.*GCRYPT_VERSION \"[0-9]+\\.([0-9]+).*$" "\\1" GCRYPT_VERSION_MINOR "${GCRYPT_H}") string(REGEX REPLACE "^.*GCRYPT_VERSION \"[0-9]+\\.[0-9]+\\.([0-9]+).*$" "\\1" GCRYPT_VERSION_PATCH "${GCRYPT_H}") set(GCRYPT_VERSION_STRING "${GCRYPT_VERSION_MAJOR}.${GCRYPT_VERSION_MINOR}.${GCRYPT_VERSION_PATCH}") endif() include(FindPackageHandleStandardArgs) find_package_handle_standard_args(Gcrypt DEFAULT_MSG GCRYPT_LIBRARIES GCRYPT_INCLUDE_DIR) OTPClient-3.2.1/data/000077500000000000000000000000001452112020400142165ustar00rootroot00000000000000OTPClient-3.2.1/data/com.github.paolostivanin.OTPClient.appdata.xml000066400000000000000000000525501452112020400251040ustar00rootroot00000000000000 com.github.paolostivanin.OTPClient.desktop CC-BY-4.0 GPL-3.0+ OTPClient GTK+ application for managing TOTP and HOTP tokens with built-in encryption. otp totp hotp

Highly secure and easy to use OTP client written in C/GTK3 that supports both TOTP and HOTP and has the following features:

  • integration with the OS' secret service provider via libsecret
  • support both TOTP and HOTP
  • support setting custom digits (between 4 and 10 inclusive)
  • support setting a custom period (between 10 and 120 seconds inclusive)
  • support SHA1, SHA256 and SHA512 algorithms
  • support for Steam codes
  • import and export encrypted/plain andOTP backup
  • import and export encrypted/plain Aegis backup
  • import and export plain FreeOTPPlus backup (key URI format only)
  • import of Google's migration QR codes
  • local database is encrypted using AES256-GCM (PBKDF2 with SHA512 and 100k iterations) and, while decrypted, it's stored in a secure memory area allocated by GCrypt.
com.github.paolostivanin.OTPClient.desktop Empty main window https://raw.githubusercontent.com/paolostivanin/OTPClient/master/data/screenshots/emptymain.png Add menu https://raw.githubusercontent.com/paolostivanin/OTPClient/master/data/screenshots/addmenu.png General menu https://raw.githubusercontent.com/paolostivanin/OTPClient/master/data/screenshots/hambmenu.png Settings menu https://raw.githubusercontent.com/paolostivanin/OTPClient/master/data/screenshots/settings.png https://github.com/paolostivanin/OTPClient https://github.com/paolostivanin/OTPClient/issues info@paolostivanin.com Paolo Stivanin otpclient none none none none none none none none none none none none none none none none none none none none

OTPClient 3.2.1 fixes a couple of issues.

  • FIX: increase secure memory pool to 64 MB, if possible
  • FIX: parsing of big aegis encrypted json

OTPClient 3.2.0 fixes a couple of issues.

  • NEW: add file chooser dialog on export (#305)
  • FIX: overwrite exported file instead of appending it (#305)
  • FIX: exported file will be accessible only by the current user (#305)
  • FIX: multiple issues related to failed first launch (#303)
  • FIX: couple of issues with secret-service

OTPClient 3.1.9 brings a couple of fixes:

  • fix db corruption (#301)
  • fix crash when user changes db multiple times

OTPClient 3.1.8 brings a single fix

  • Fix importing Aegis plain json

OTPClient 3.1.7 brings many fixes

  • Add new Database info dialog
  • Fix crash when no row is selected (#295)
  • Fix UI when creating/changing a database
  • Multiple fixes when creating a new database
  • Use current db folder when creating/changing database
  • Fix memory leak in case of error when opening the settings dialog

OTPClient 3.1.6 fixes a security issue.

  • quit the password dialog when either the cancel or close button is pressed

OTPClient 3.1.5 fixes an issue when dealing with symlink

  • allow the db to be a symlink and follow it correctly (#289)

OTPClient 3.1.4 brings some fixes

  • make auto-lock and secret service mutually exclusive (#279)
  • fix importing plain AEGIS (#281)
  • fix importing encrypted AEGIS on some distros (#281)
  • rename disable_secret_service setting to use_secret_service

OTPClient 3.1.3 brings some fixes

  • Fix Aegis import/export when using long pwds (>64 chars)
  • Fix secret service cleanup
  • Show the correct error message when import fails
  • Use g_utf8_strlen instead of strlen

OTPClient 3.1.2 brings compatibility with newer cotp.

  • Add compatibility with libcotp >= 2.0.0

OTPClient 3.1.1 brings lots of small under-the-hood changes:

  • Fixed some memory leaks
  • Improved error handling
  • Use secure functions instead of standard ones

OTPClient 3.1.0 the following feature and fixes:

  • New feature (#258): entries can be displayed as a QR-Code
  • Updated the artwork credits in the about dialog
  • All keyboard shortcuts have been revised, be sure to check them (Ctrl-k)
  • Code cleanup

OTPClient 3.0.0 brings some exciting news:

  • New feature (#263): OTPClient is now translatable (open a GitHub issue if you want to help translating it in your language).
  • New feature (#259): it's now possible to edit also the secret.
  • Improved: an About dialog has been added in the settings menu.
  • Removed (#257): Authenticator Plus support has been removed.
  • Various small fixes.

OTPClient 2.6.4 fixes an import issue

  • fix issue when importing encrypted AEGIS backup.

OTPClient 2.6.3 fixes an issue when setting the migration flag

  • fix ternary operator logic that would incorrectly set the migration flag

OTPClient 2.6.2 add an upgrade message

  • warn user about new secret service behavior

OTPClient 2.6.1 some fixes and a new feature

  • add ability to import Google migration QR also via webcam
  • fix double free in case of error

OTPClient 2.6.0 brings lots of new features

  • add support for importing SVG tokens
  • add support for importing and exporting Aegis encrypted backups
  • add support for importing Google otpauth-migration QR codes
  • improve enetry deletion workflow
  • add support for libsecret
  • show video feed when importing a token using the webcam
  • fix andOTP import bug

OTPClient 2.5.1

  • Fix markup on change database dialog

OTPClient 2.5.0 brings load of new features

  • NEW: rows can now be sorted. Enable the sorting mode by using the "up down" arrows button on the top, then drag and drop rows where you want (#184).
  • NEW: added a dark theme (enable it settings) (#207)
  • NEW: allow to switch database. This is useful if you have multiple OTPClient databases (e.g. work, personal, etc) (#186)
  • NEW: add a button to lock the app (#236)
  • FIX: when app is locked, the content is now hidden (#235)
  • FIX: it's now possible to change the database, if the wrong one was selected during the first run (#196)

OTPClient 2.4.9.1 fix a regression

  • fix importing QR code both when URI contains and doesn't contain one or more UTF-8 chars.

OTPClient 2.4.9 fix an import issue

  • fix importing QR code (issue#240)
  • better error when importing a QR fails

OTPClient 2.4.8 brings a couple of fixes

  • fix show next OTP option (issue#234)
  • correctly decode URIs from QR codes

OTPClient 2.4.7 implements some small code optimization

  • do not use strlen in for loop
  • do not use strlen to check for empty string

OTPClient 2.4.6 fixes some small issues

  • check for NULL when comparing account and issuer
  • use secure_strdup when trimming the account key
  • fix account/issuer when importing andOTP db (tested with latest available version)
  • use g_memdup2 when available

OTPClient 2.4.4 disabled a broken feature

  • remove possibility to sort columns by either account or issuer due to GtkTreeView issues. This feature will come back with 3.0.0

OTPClient 2.4.3 contains some small fixes

  • fix a small andOTP export bug
  • fix wrong icon in taskbar
  • remove hard-coded paths from get-builder.c

OTPClient 2.4.2 contains a small fix to andOTP handling

  • fix handling of andOTP data when importing/exporting, thanks to Michal Borek for the contribution

OTPClient 2.4.1 bring a new feature to the CLI

  • add export command to otpclient-cli

OTPClient 2.3.2 brings a small fix and a new icon

  • fix incorrect code is shown when sorting by label/issuer
  • new icon, thanks a lot @bertob

OTPClient 2.3.1 brings a security fix

  • fix a memory leak when exporting to freeotp format

OTPClient 2.3.0 brings support for a new provider

  • add support for importing and exporting plain json Aegis backups

OTPClient 2.2.1 fixes a long standing bug

  • fixed a bug that prevented andotp backups generated with a long password to be correctly imported

OTPClient 2.2.0 brings support for FreeOTP+

  • it's now possible to import and export FreeOTP+ backup (key URI format only)
  • minor fixes to the first startup dialog

OTPClient 2.1.0 brings some minor enhancements to the UX

  • save sort order on exit and allow user to reset it to the default value using the button located in the settings menu
  • on first start, allow user select whether a new database has to be created or an existing one has to be imported

OTPClient 2.0.1 is a minor release that brings some fixes

  • show dialog if memlock value is too low
  • fix memory leak on parse-uri
  • fix a double free in case of a crash
  • better error handling
  • multiple fixes to db handling

OTPClient 2.0.0 is a major release that brings tons of new features

  • add plain text import/export for andOTP
  • add lock feature
  • add CLI (currently supports list and show commands)
  • support import/export of encrypted andOTP backups generated with version >=0.6.3
  • treeview can now be sorted by clicking on the account/issuer column header
  • QR code can be added from clipboard (supports both gnome and kde)
  • minor fixes

OTPClient 1.5.1 brings some small flatpak related fixes

OTPClient 1.5.0

  • add shortcut to quit the application
  • use native dialog for open and save actions
  • show error dialog if database is missing
  • rename "label" to "account name"
  • correctly handle empty label and/or issuer when editing a row
  • respect XDG_CONFIG_HOME

OTPClient 1.4.1 brings some fixes to the flatpak version.

  • fix setting menu not being accessible
  • fix window size not being remembered

OTPClient 1.4.0 brings full support to andOTP.

  • it's now possible to export encrypted andOTP backups
  • use monospace to show the database path on startup

OTPClient 1.3.1 brings some fixes to bugs that were introduced with the previous version.

  • fixed a bug that caused a row with an empty issuer to be treated as a steam code
  • fixed an issue that prevented the same item to be deleted and added again
  • the correct password dialog is displayed when importing something while the local db is empty

OTPClient 1.3.0 brings a lot of new features and fixes.

  • reworked UI
  • support for custom digits (between 4 and 10 inclusive)
  • support for custo period (between 10 and 120 seconds inclusive)
  • support for Steam tokens
  • add keyboard shortcuts
  • add settings menu
  • search by either label or issuer

OTPClient 1.2.2 brings some small fixes.

  • add "native" support for Ubuntu 16.04
  • add shippable support

OTPClient 1.2.2 brings some minor fixes.

OTPClient 1.2.2 brings some minor fixes.

  • it's now possible to edit the label and issuer fields
  • when a row is ticked, the otp value is automatically copied to the clipboard (which is erased before terminating the program)
  • a small help is shown on the first start
  • 3 new ways to add a token in addition to Manually (screenshot, using webcam and by selecting a qrcode)
  • some bugs fixed
  • cmake related improvements
workstation mobile
OTPClient-3.2.1/data/com.github.paolostivanin.OTPClient.desktop000066400000000000000000000004271452112020400243400ustar00rootroot00000000000000[Desktop Entry] Type=Application Exec=otpclient Icon=com.github.paolostivanin.OTPClient Keywords=otp;totp;hotp; Terminal=false Name=OTPClient Comment=GTK+ TOTP and HOTP client Categories=System;Security;GTK;GNOME; StartupWMClass=otpclient X-Purism-FormFactor=Workstation;Mobile; OTPClient-3.2.1/data/icons/000077500000000000000000000000001452112020400153315ustar00rootroot00000000000000OTPClient-3.2.1/data/icons/com.github.paolostivanin.OTPClient-symbolic.svg000066400000000000000000000022171452112020400264170ustar00rootroot00000000000000 OTPClient-3.2.1/data/icons/com.github.paolostivanin.OTPClient.Source.svg000066400000000000000000001426231452112020400260450ustar00rootroot00000000000000 Adwaita Icon Template image/svg+xml GNOME Design Team Adwaita Icon Template Hicolor Symbolic OTPClient-3.2.1/data/icons/com.github.paolostivanin.OTPClient.svg000066400000000000000000000112471452112020400246030ustar00rootroot00000000000000 OTPClient-3.2.1/data/screenshots/000077500000000000000000000000001452112020400165565ustar00rootroot00000000000000OTPClient-3.2.1/data/screenshots/addmenu.png000066400000000000000000000463161452112020400207130ustar00rootroot00000000000000‰PNG  IHDRõN®­ž:sBIT|dˆtEXtSoftwaregnome-screenshotï¿>'tEXtCreation Timeven 10 feb 2023, 16:33:01‰#rØ IDATxœìÝw|uþÇñ×Ì–ôNBB ( Hµ zöÓ³W,(Š…Ÿ ³ÞÙNÏz–Sï=õlXQQTŠté!¡%@H!¤nv³»³3¿?R a“Ý„ô|žÇ>v³;;óÝÉî¾÷[æ; „B!„B!„B!„Bo”Î.@]©,B!DK]èü íìí !„m­Ó¾3BU‚\!DoÑ¡ß{$ÛB!ºš# êv ùö M×Û’íKÀ !„è,- c—mó€oë ôg}M-s$ÏB!Ú‹?áÛÔ2GòÜkËln]Þ;’û„BˆÎà-€ä>ó[[fK¼¹¿}-Û’í !„GÂWÈ6~Üðó1_ë>¢po¯Alþ„wSîo¸K¨ !„h/þ6§7æ†Ç›Û†¯ÇšÔ¡î+¬›ºnê>·%„B´…–zsaî+ܤ/þ0­ Å–zs×þ,Ó’m !„GŸõ܆ŸË´d›~iM úÌæoˆ{û»¹çû³m!„âHøàæo{û»¹çû³mŸZˆ­ ô¦Â¼¹Çht»©í!„í¥¹&s_ÞøÒÔ²Ím§¹û¼2û» - t_!îÏÅÛº|•G!„h ÍõŸû ðæ‚ÝÛv”+^–õvŸW- õæ´4ÄU÷5^WÃm4¾ÝÜ}B!„?ZÒìî-Øuqo÷y ùÆÁÞjþ†zs!êO «^n{»ÏŸš{seB!Ú‚¯@o|Q94Äujrªñ}ÍmO¡é€÷+ô[[S÷7иÚÌm_áîm»ÞÊ$„B´Ts5u_¡®{¹nÐz£u4Ý'B½©¾ì–zã‹·û› w¼ÜöUN!„Â_¾BÝ[°{ óº@oÎuÁNíµJë‚ÝgÐiŸzãæð†î-¼MxùD`0ˆb€€&¶ãOY„Bˆ–ð§VÜp'p(6kýüì ¾©õè^î;¢¾õ–†esµô¦šÚMÍÜN®Ú§OŸˆþf³9Èd2YE2Z!D×dÇ­iš£ªªjoqqq¹Ýnß|d~ö¦n{«í·æ·z¾’³¹¦wšÛMM\[ë‚‚‚&'%%¥õóx<¸Ýn Ã@×u ㈠!„íBQTUEQ, f³‡Ãq`ïÞ½;ÇOÀû€‹Cƒ¼ñµ·poêøö†š H“¯r{¹Ý8Ø›ë;7qh ›€pàÏñññ'öïß¼aá‡MÓ$Ì…Btu•PMÓp¹\˜ÍæØØØ$UUCl6Û`ànðš ÷¡ûû< ùP÷·–ÞðÜ?`À€qQQQÇ8Åãñø[V!„¢ËòxÈçÐll\c÷ìà=н½êG™|MÉêÏHø£Ífs?o+·Ûíõ5tѵDDD`6›Ñ4­³‹":‰ÛíÆd2uh 7ˆÉdjö}èv»±X,Ú…&ŸÑ‹ÅÒ8šÃgUmjZtð½^ùêMiªÖî­¦Þö¶‡Ã!Þ……‡‡Ë—T/æñx|Ö;B@@@³ïCÇCXXX–È;ù¼ˆ&„P“ƒM Å[ž¶JkFv4UKo®>RQ³·¾&Ç#¡Þ…IaïVÛ'ØÙÅÀl67û>4 £K|tôçEUU¬V+ªªb2ÕÌT7HÐåruØ`A)_óE1Q4æ/õO¥…æZr>õ¦~IøÓ¯@¿.à_^6ô-}…M ·÷5`N´#Åb0ÞIÀ˜j0­?E€8rj #Ï<ƒñi‘ä~9›…{{ç ‹&NœHrr2«W¯&//€¤¤$Ž?þxúöíËš5k:¹„ÝKXXX› X4›Í„……QQQÑ¥ú”Ïo- ×ð¶ßßê­}•Mýªè”šznn. hÏMtY¦DàsªPÃŒÒT âÞÒÃö(èÕàÙoÁ¹:OQçjîqÌ£¸úopAð>Þ]:‡…{qÜ5û%®ŽÛÊ+ÓïàƒmwÈ“EbJZÎvò»È9’RRRHIIáË/¿ÄétÖß¿{÷nöïßÏE]Daa¡ÔØýd±XÚô°Â€€,K› N“òùÍß0o˜¯-v¤‡´5.HSMñ¢+0¨! qaä¢jnžüîìJXgßpWüaÓ"1»JÙ—¹ŠÍæÝ…YTa\ðÚžžÔôÚµì&Ýùg½ôÿw|A*†Ç…½¬Ýé«Xðñ>Zºêúmá¬)S¹üÌ Õ?†`ìíIgõïóúì¥äyÙ†)þXN<*ŠPÓXN<*„w–·IcŠ~/ÿüw&[¶òÒEW3;»k´ Œ5ŠU«VèuœN'«W¯æ˜cŽéôP_¼x±_ËvÚiíZ_‚ƒ½NñqDBBB(++k“uuçò©ªÚäakÍ=våk®É½©l=â>õ¦BØ×ýÞ®%ÔÛ™'ÏŒíÝp¬c« [R;Ceù‹Q‡/l60…XÇ8°ŽtxB5UŸ‡vlÛˆÚg¿ý—§ýþ ˆcðø <î Nï.nzn ºÇËU³„j²`6) ·§æÃêr{0$º_<‘A*žj6—‰à¨þŒ“yøígÝ&á$ ;žóÜ«˜={©×²z²?å©§B9»Ï6>_Ø6€¢b6u½Wlll}“»7yyyLž<¹Kä]g‡µ?ꎣnk‹¥ÙÐò—¯òùúáÔÔÿ #ʧª*Ï=÷?ÿü3óçÏ?ä±óÎ;É“'sÿý÷{-ÔÏ[ {»öö<¿¦Šõ§šÖÜF|¹„z0Ü Î•A¸60®KZÍBš‚§D¡zI0Ö‘.LñÝtæ+%’3fýËRƒ0*ÓùèïOñÎâlª"†qÖô‡¹ÿÂ! »ö1îZ~Ý5o°2ù™e¼òÇ?Þlj3áª[Ÿ)­ö†‡Œ×¯âš9»Q¢Žá¦ÿÍãc8åšó8ÿ3ÿ¥v›¶m|öÜ3¼¿t´Pú Ã0V“ãÁûÉŒ-£¹òÞ›¸ xÆŠ…<»QSo¸——ŸÆð8•{7°àíyyÞ6ª À4”+žü3—ŽJ!>&‚°@ƒÊ}[øéÝçxþ³Ll ?ÆæÜóÍfîÜ¿=Í97þüNœÓÃWÓdÝIW„oí9¡ŽÕj¥ººÚ÷‚>ÖáKSÁí+ðÛ»|º®óÓO?1sæLÌf3óæÍàÜsÏå¾ûîãå—_n6´[Y¾æjèþä¦Ïš{K&Ÿi) ófØUª—S½ÄÇrµ5W% {ލSúüË&Ç Ö¾zO½ â7æþu1ÃærǰιøD^X¾[+¶¡•naþâ]Ü6~4jŸ8úô9ƒK'E£Õ¬}õ.ÿ¬v›”S±2m-Z{ãî}‹7®ŒÅUF^a5Ñ)'pÍC‰q^ÂÌï‹1Lý8vÒŽ 5pÚJ(³‡“2Ëy=ï_aÿ}uF5¥yù”¹A˯ k4‹¶P7ËY{h‹Ézº{ù¾ûî;fΜ ÔœC`æÌ™¼üòËõ!ßNåkm¦¶ûä37Øøv›×Ôsssý¾¿7 ž ¿½ ¨x#²³‹ÒaLɃI±(àÉfùÊ|ù-­e±bÕ~n6€ ”Aô3ÁŽ–¦œ)è'põ¹C1ž‚ý¨ÛænV®j´ÍRûžÏ­W¤bqm⟗ßÈì]ñ—ý‹y=‰É—ŸIÜ‚)¬[XÏããéçóì–¾\ýŸ¯xpb_&MÉÓ+VSßÎâÙÅoë}êÓ§O?äÚײo¾ùf{©IÝ¡O½=gÉk‹@î åûî»ïP…û^xá°æø6,_SáÚ,'[{H[s£õÚµÀÞ‚º3G¿wöƒØÊŽݸ!EQjßPF½gfä½ß²ùÞßï1<…,|o>{=SÛl[æáÇpt ‚¢Ã=óÖsOƒÇ< I$¨üêõ°iKúÄADÄFaºj牿AíOð·§îЧ.z_ߦkëCÚüÕ“Ã?Ýõ‹¡;×ìõœlr4ƒk*'LŒçíì¼ßkÎæA?¡*U¹»ÉoQåÕ¨Xçtâ(Ë'+} >ü/Ÿ®-¿—ýºA¢9… ãûòÖ®ý­¯­«j͉”«·±àKjúâëJP¶ž¯³;h®ÚQª©æƒeÔý¨QQ{ü'­wjÏž´ÅÌm=¡| ûÐëšßö±·CùÚõÓÚÇ3É×M;2\ ŠÕ@±õ}åÞ„M+ǽÂsm †½{ϤXÄ—Ëf0þŒHÆÍxŽ?—=É;Ër¨ŽHãÌ[å¦afO! ç-oaº‡­/_Ì5svÞ/]´„Eïaì¸ Æýß‹<`–ÿ-ÛÉA—•¨Gs\ß¾ÿy.ÃG7@‰ >>…ÃG»k»¶±K;Ÿc-ÑXóàí¹;°é`J$ÒµŸ"?¿' g%6—Á ‚’Ub6ƒ¦Q÷@[««‘wfS{wå+8Z;ºÚ&ÛkJWè˜ò{î¹^ûÐëúØ› öV–¯Ýó°-N½êkÑŽ<ùfÌÉnO³ãü-£RÅp¾ûÕ0€1N¬£œ8 Äõ[ ×庣˜ïŸ~‚‡>ÍûÔç>eÊ!W“õùã¼´¤²íšçõ}|öÌ«Lž3“ñ‘#¹ú©÷¹ºáÃÅ_R¶êa–8sÈÚ£aΙOÉ_«Ïço+­jïîˆòéºÞäqèóçÏçûï¿o² mU¾f´:_›šÁ¦á톗†g—15¸˜k/–Ú‹µÁµxuРAÇx+@~~>¡¡]c“ÉDrrrg€œœ¿ËbŠÓ_©Ÿ†˜|G‚^¡bÿ.Ï~ß¡ž““Ó.³F‰®Ïn·í÷ò-ÑÞÒÑï%%%M¾ív{—úì¶åçÅl6åe2©#PZZЦµÍ/?)Ÿ²³³73WíÅÝàÚMÍOq ð4¸è .F£KO>ã/©¹wO‘û·Íÿ0Џ§”š‰j\ëq® è¾ÍïBô"š¦át:Ûlþr§ÓÙf R¾6Òi‡´ùC’¢ Ò+L¸w™qý„^%ÿ"Ñ~:ûPµž¨²²“ÉtÄgÓ4ÊÊÊ6*Õï¤|­Öæ_ÆíÙ¡*ÉÑ…TÎ ïì"ˆ^@F¸·Ã0¨¨¨8¢s‚kšFEEE›Ÿ«¤|­ÐnùؽmBˆ^ÂãñPVVæõìw¾8NÊÊÊÚõ4)_×Ð=‡> !D/TWã4›Í„††ú<1ŽÛíÆf³µG°”¯‹êôPWUÃ0Pi­ïŠt]—ÿM/¦(J—ø|ú*ƒ¢(èºÞ®s‘û££>/š¦QVVVjQUUëç"÷x<õ‡rµóaWR¾.¨ÓCÝd2¡ëz»žíG´žËåêô/JÑyEAÓ´N?Uª¦i;EÁårØ¥:\G^t]?âÓ“¶')_Çëôoë   6™9H´ŠŠ ùÁÕ‹™L¦VõA¶µêêêf߇&“‰ŠŠŠ,‘wòy­ÓC=88Ã0$Ø» òòr4M;âÃ@D÷e±Xðx/¢+èôwŸ¢(ÄÄÄPRR‚Ãá¨ïßèì>¼Þª®/©¢¢MÓ°Z­írˆ‰è>¬Vký„˜Íævÿ|†¦iõîÏ4©V«•ªª*œN'áááõß%íI>/¢«éôP‡šiübccq88œNg§ Pˆˆˆ ''§Ã·Û”Î(‹¢(õƒF,‹|A E! MÓ°ÛíèºÞîïCg3 £¾Æ^\\Üáå”Ï‹è ºD¨C͇#88X梋2›ÍÝ¢i¹»”SˆöÐé}êB!„hêB!D!¡.„BôêB!D!¡.„Bô6DÔn·wÔ¦„Bˆ^©GÔÔ“““‰‹‹“9Ê…Bôjz0gDDD»¬×ív“””DBBåååTVV¶é©ò, n·»ÍÖדôô}ÓÓ__g’}+z‹Žœ¾¸GTmív;𦑓“ƒ¦iõ/Ù!„èMzÄ´Ku'¾'??Ÿ¢¢"ÂÃɉ‰!66–ŠŠ ***ðx<]T!„¢Ýôˆš:@ee%QQQ(Š‚a”——“Mvv6 66¶ÓÏ·,„B´—QSÐ4 ‡ÃAdd$¥¥¥õ÷;NòòòÈÏÏ'22’>}ú ª*eeeØl¶N9qŒBÑzL¨TTTsH¨×Ñu’’JJJ "66–äääú¦y°#z5%ˆ¸AIŒ¢|DzË{ÈÝV¾.%0šÄ~±DZ+ÈÚžCN¾&º‰Óüàp80›Í>È9rssÙ¾};ÕÕÕ$$$””Dhhh¯?»b""&†ÐõsOø¤Æ1âÔ“™0:¾A]˜6äíuY˜pÑõL½öã“B 0+`踫«(+ÚGÖÖlÍ­à5•@Ÿy5§´BÉo|þéo7øŒ¨Qc¸ø²qôQªÈøî#–îm»Ã<ÛÛ೦¶ÛëRCãIŠ Âª$п•­%Õøúj9ì9UÚö³"Dé¡®ëêòSQTTUÁÐut£æ£æiA¿·Íf#99³Ùì÷±ê†a`³Ù°ÙlMÿþýq:”——ãr¹üX‹BÔQ#I2è: Åàßö‘YÕÚí P ]70ü,¶Ñ삆¡£ë¾–ë¾” dNþãm¦¾ÇBT¿¡ŒMDÊæ|»ªÈÏ÷¸™àÐPÍ ºæÆíQ°†—|±ý’°øsì°ý@F5{wæâLL`T*cÖQ\Ÿ~*‘©‰VA¯ÜÍÎýÝ'Ðv}]zY&¿þj%5ø ™Ù¾ÝÛs0)¨½¼UOtM]"Ôw.x›˜H™|=g¥Yðd/äE»ñ(A =û®¿uUóùhé>4bÆ\ÌEÇE¡í]ÆÇ ròx†ÆEÄÊ_—ÅZ\§ÓI~~>DFF[?°®²²²éÚ»¥?Çãq’[Lb¿8†¥E’¹îàïµS%€ØacsT }#Qœ•e,ãÇ hÍ=¦“pÔ8Ž;ª?±!fܶBrÒ×±v[N£¦†}æÕ§Ð_9ÀÚ/¾aS$œp9ç‚sû>Z¶]‰fø)AH` pV‘›¾šÕÛâ2@1t4MC3EqÜ%7p ç¯dîw[±5ñÍgF3ƒ <š†¦æ©YNG!tÀqL“F¿¨ Tw5Ue»Y½h¹š|l¯>„³®9•êÖ~þ Ë ßIWsþˆ\Û¾çý%{kö³BÒ±;<™ØWy!Y›V²zÇAÜ F:a#OcüÞ¹,mî=^·>5º~¿¯û‚¯6”AP_ŽýùŒëÄ€‘CˆÜµÒåtîÝINu*Cƒ"IMe]qQí~ˆ&uP *åY;(ôXH'¦ElEÕ”f³qåjv”xF%`(ç\7‰JQmmô÷}îÌüÎÿ}Þ íùºPûrôÄcI3W`ìËfeŽÇQ'2jP_ÂLNÊm J3ÏYU7lGc•·0Ð÷/çÃow‘xÖ5œ–lž1Ÿ—îÃBÌØK¹tl4ž½K™=?³u;E?t‰Po–Q;Ü<ý ŒK ’½”šÃè`cËæ½ØJ]$¥ÆàÉÏ"#¿æ)n·ûˆ'ž1 ƒÒÒRJKK !&&†””*** $(èðŽÇÂuË(¬½½ †Ái1‡.ä)&{K1Ùõw„3(-Ü÷c®|vn̯ €H’‡DÖÿµ{ùRvÄ &-(^ϯK‚œ–V³úûèw1¤ ‰9|= ô’vøò“më¯, ŽÁiqµ÷V’»u¹ – ì?„´flÀÞuk)2á ÇdªÂÐ5t]oÐ `!áøó9gdª^M¥ÍMPd"#N;Ÿ Ï\ír`(‘$D? èîj3†“Öœ™@ ÈQ)(†Æþ5‹X¾£¢¦ÖçÜOÆ’Ÿ޹„1}B<¼?«öeáO{Ocº£ˆ]9¥ŒíA!‡ uïcÛ®J†Œ '|P*qk‹(ÐAM%5Jã ;w@GA·†bÒpTº±„„Õ§…º)ýt -ÿù[§ù}¾p—£u«íÈ×¥„2dÒ¹œ”º›j‡JXx*øî.24•6œ:è6'Fí÷•> ‰àøD¢•} „„~‘`èåîmÝþÂO]?Ô1¨ÊÙÃþñ}‰²ô!6©/á–pŒŠýä—“¿o?UN §ýš«ªª¨ªªj·õ ßL&3ÑÑÄE‡aRHNŽF¯ÌÅ^•‚ª†6„1GE¢ê…¬ùìk6”„u6—ŸÜŸ”£R ÉJÇV·B£‚-ß}ÂÊUUhÍ‘Jd4‘ªF){÷ÙmÆÕKÙ»ßÆq}Â1GDªBI ·a¨f‚"’8:µ `Ømþ.ôP¸cå#Ž#*,•Áñk(Ø}§®x v°£Ä òW|Ì;ËU‚± âÔ?ORD ! Zy¾%%´ù}ήôÖ­¸_—>˜£ ålýî ~ÝçÄ:ô®›”ŒÏv£„ÍßÚ§®äæP¨'Ò/2‰¤ÈµÛHŒUÁ("7WNl%ÚW—uÃ00*³ÉÊJÁTœƒG1Èܾ ]kM½GtWFÉ"J`2™HȦ›HR1 5:Ž>*(j_&\1 ž¯‡„ªð{¨×«CÐ*FÝW¾ç·¸ëU%n•ÜÚà†ìÍ;©ðòÃ@/ÞÆ¶Âc˜ÂÀ!ýXUd6(Õp““±ƒJPC0î4N™H¨¹AÓ²nÁbn}ß°O¬IAQ¼ïó#ÑQ¯K‰Š&JQ0ìyìÞïÄ€#š}Ò°í&»p 1 ÆÖòþ$˜Òøµ IDATŒ{ØS)êDû겡nPè†Ad¤gÞNÊKŠÈËËë좉.Àãñ½uÎÄDŒØh""#©0<`èî²Ós¨hµFu~͸€6ÛdT”Rf„ªÑ$%…°±¬ò÷xW#Iꊂ»¼ [‹¾Ëkz<ÜÕ6J‹ö‘¾™ŒýUÞ>•ìÈØË˜¾)FÚƒ!*†#›­»€5y“ŽM"ÀUÈÖU[)p÷áè“F’`¢É}b`ÔlO1a25±ó”š{ í Y[ßçG¤^W“TÕïçÔpÈCUìޙτ„$ú Ì’DÐ9°{·×eB´¥.ê†a`Ô6©†††IÖÎí8«¥éJ*//{E) ýSpWqÀ=˜D³³m3Jp` #P³ÑÖ"öl¶çŽ#q` ýÆýã«—±9· w` ©ÇÂèE·±{{Km48°æÓhàØ½•]ãpTÈ@Ž?ÞÀ¬”mÏ`Ÿ@%("«úí¬ßº»b§ßÄÚð0 P µ àÄp;°»  §oßÔƒ‡þô’bJŒ!Ä«^÷ù‘i§×åå5Ô‡˜Ìð!ämó}F-Csãö` %&Ê‚RêU]ÇÀÀžI΄$RãŽaB´ô|vf•÷ÈC:E×Ò%C@7 Lf3}ûöe÷îÝè¢I¥•vܹ¹$'§°1³˜¾#ú’|â¥\?Á‰Ë°hÕØñýûü’ÛÆ'ô1d-_FÿèÉ ‰ˆcÔ—0êÇ5J·-cUîáAÒæÜûHÏ,aØØ>X,`¸÷‘¾õ@mˆèTÀ®Ç’x<]”J¹+€¨€†+(§´BLj `ФË8Ùý1Ks Ù½§Š¡ÃCI:éJ¦çD >¤bjTîdÃŽœ54œ”“.ãú Õm»ÏÛãuí;tFå6lAßáá <õ ®Ÿ`GSƒQi¦cE/"¯ÀMê€ Rϸš»ŽÙµùŸ­¢HÙËÖ• ŽÕbàÎÝIveÏ<¬St-]pF¹šCŸ4NLŸ>”••ù5‘ŒèÝl6ee¥¸v¯ã»Õ™ä•ÙÑMVMª ÒcjË–÷zº-‹_¾üŠ_6d‘_æÀíÑñ¸ì”æïbýÏ_ñÕ²œšbÔ 4c39®šÁcö¬Íìlp¢–·†…+¶SPé!(¶‰‰QXœØ¿Ÿ§z9[—üʶür\† §Ã¸Ø»r¿fæSá‚€à LÅyäÕÖ 9K¿æ»5ÛÙ_^a8dŸwÍ×ÕxÕäþú5߯ÝI~¥ 5 ˜@“{Yyûâôöÿ3ªØ¾ôg6å–P­[Rq¹<˜ê¿Q5 Ó38 `T³'3»dºèÞ¾ç”F·^ÔSƒ‹°Ô^[ko7¼~5>>þ˜ˆˆ¿ eªª’’’Bff¦œtEøÅd21lØ0öìÙƒ®ëm2å¯Åb‘ó´“žºokÎ AɧqñYi„Vlæ«OWR(g~îµÊËË)((ØÌ\€»ÑµV{[< .zƒ‹ÑèR矋]®ù½np\XX•••èÂo›ÍFhh(åå5ý¢½}.ÑÑ, :ý*NŠW0‡b¡š¬ß6Q$.:Hl~§~€\eeegE(QwÅ=Ü~fmÑ Úž*++ í±ÓÒŠ.N &Тc @©.!{å,Ͳ·ÿ˜ !ju¹šz‹Å‚ÃÑÊÙ¨º óhîýòMÎ\ñüééÕTwvyš¢ÆqÂÕS9Ù"þóã>ßËw"»ÝNß¾};»¢·ÒËÙúýÿØÚÙå½V—«©×5¿›L&'dQq{g>Ë×o%cëzVþ8—×fœLŸ.÷ªš`Ø(ܳ›ìü ¤u®mhš†Édª !DoÒekꪪ6êJôÙ<1ûoœ´ÿ ^ì%²Ê¢RŽa¤^I·™´É³“f\É]ŽDÓ4Tµ»üªBˆ¶Õm¿ýÌi^À—ÿ+oÏ[Ä’Å ùê¿ÏóÄ{qÖ-dêÃÄiÏ3÷—ulIßÈêEÿãŽÑVÀÄÀ+_㇕ëIÏÈ`ÊoxóîSé[×a¬ÆsÖÃÿeÞ¬ےAÆ–u,žû×vøá–‰üí×t>™: ~gªI×óÑ–eüu¢”P†_ñ$ýôé[Y¿|>/\”X³¬i(wÎÛÂ3GÖüºòg»j ãnü-ZË–Œ 6­]Ì7¾ÂuG5ü}ÀIO,gë7w1¼înëÉ<¹r ï]Ó·¶œ Q—ü‡Më_åÜ0ÀÜÓf¼ÂÜ…«Ù¸ù7~þèi®ÑðõªDŸ<“÷~\Ë–­Yñí¿¹ÿ°6ÜGº¿­dý– 2¶¬eÁœY\så=üó“…¬Þ´… Ëçñê´1Dȸ7!„hR‡ÖÔ-K³×5™ú3ï²gÿöj—qâ§Ñoó"ö6| #ï˜Í›7˜XøÚƒü3½kl{5À xÃ'<ß *â¿™‡î~–Y™gpß‚J %‚!ãÆ³ó%f>ºgp2“¦ÝǬWf±û܇YÚ‚‰²Li7ðÌ#“(|õ~®]Zˆ;ˆðüÞg–ò¹Ý@FÞ9‡Ù7²ø_ãöMQúËýOÏ1ýTȨ[‘“M«Öã¼p£"2˜SGsL„…øcGðA!¬Œ=eË«¬« fÌý³yù¼¹?ƒ¹ÿ|žû^²3éN½çYîÜp6Oýæû0(³ÙŒÉdBQ”#ïë}+ZOö­m«CCÝŸcRu]÷ë06=÷#zt/>ô NÏdñ¼Ïùøãy¬È­;l7_7˜o\ÀogÓ¸!¿rû2ÕÞNÏ|‘!ç~ÉÅǦbZ°±vY[ö–­Ü‚Æ*Ö$râÜ 8å(3K×øF¸š“E”³|Íj6fÚ!3ÃÇ3šÙnæ©ÜtÝö̾”™ÿÉÄ (Ѹ^?ÿ°µØÖ®`3÷2nt0Ÿ,r;v,‰öJ=žáæŸXOƆ²ëËU ;ƒG¯èËòǮᵥ@æc‰œ²ðÎû8K–xÈÿi6o~º X¶r'©ßpë5§ðÊêï©l‹ý½g=+ÖlAc=ÉgqÚ´],úx¿º 0î‚73¦?ü–/šVsZViŠBô&ÝøÏIöWqÑ)gpÝ3 )1}ÿ#ïÞ>†pÌ©#”Ïoks °ÒÿŒ»yå“Xºfk—ü‡k“MX­^¶SÓ—Ã>=’舖í2mý'Ì^Îõï|Ë{OÞÂÙã[ôKªáv̓F1<¨€UËwúœKÜ(þ•ÅŒ;a$V%ŒqÓØ2ç?¬ŽœÀÄA&LIß/‡%KsaàpÒ‚B8íïËØ’žNzz:›þ…1–bcC½ÏÄæÉeÃÆÎs[ïo `GXwW1… $üÈÎü%„=Y7õ†c?¾y“‡¯;«^ÎaÄã†afPÔ&§5¥Måå§°å?Ìšz×Üú ?ìo~¤´áÑð 6˜²î§¦¹×ëö\Ûyï–³8÷Ž9l¿Œ§¿ø÷oA ·e}mW5cBCóg¨¼¾Ÿe¿ì$ú¤S9:l'ÎaÙ‚oYš>€“ŽO$þ¤SHÛ¿„_vj (`àÛ.æ‚ .¨½œÏyçü‘‡(oò[EQÀÐkÏäÕ¶ûÛíÖ0SÝi³ n!5o!„hFú†´³ó×µDJ’ŠgO&; Œ7à°šq@ÚH†¨ëøèÕ/Y™¾ƒ™éä”·rȼQBq©Bbj2MÖó*r–ÀS7ÿ‰›Þ=Ȩ)—1º]‰žÜ,öxú1úØ?þqö,\À΄ɜÃYL,^ÁòÜB~]²ƒa¸Œ+ÎIÁO ÈÐÀ³g»œ1 M6±7;›ìúËn *›øaƉãc©ÊL'W£ãö·Bˆ&uÙCÚ|±{O^̆U[Ù{Ð9˜S¯¿‚Õy?CÃ(ý‰ÿ~6·ïxg•×™·©wX"Áy?°8k;{”ë¸âŽ )œ¿ƒ-ž¤ÐV¦òìfñ¢]Ü1m&eX™·½’†U›º¦“¸r‚ÊŽmû±[ú1!5ÊJ)kÅ!ÔFÉ|ôý¼zçó<èx² úO:—a&Xå­h9 ˜Ÿ1ƒ{o‰g÷ì«Ø©éè?ÿÈÎ{îã&u7o=QÓT^ºˆw?›Îì›_âEÏ|¾>gPC"rùbÞjÆ*„ ÏIq&1ñÊLMÊàYKkï¨ý-„¢IÝ7Ôe”ÇœÅO%)*½ª˜Ý[òÌ´—ø$Ol¬yv*·•þ™ÿ»ê1^»;wi?>³’Ÿ¿™ÃŸã‘iñæ”PLn;åÅÙlÊ­hEI42ߺ‡¿D>ʌ۟ã¼pΊämZEf±Ž9f(gÞt‰U«`ßÖŸyú9lÓ Ås®eüüÄ­<ê˜Å 3^âÊp'{6ïÇmèÞ~$xrùö‹5ÌÂ¶×øÞù.ý^Ž6}ÉW;êz¿m¬úÇÜ^z?w^ò0¯Ü¶¶ÿð?~ 6£œkÖQ|ú-üsN8V­œÜM‹xnê ü/ÃU¿ŽŽÙßB!šÒåÎÒV7ú}ذalÞ¼¹•/«÷0¥N糯.aÕ”søÇFÿGå÷d£FbÛ¶m¨ª*}ðBˆN׫ÏÒ&šcfÈY×0ZÙCNa9žˆ!œqó Þ÷9gJ  !Do'¡Þ(¡${&7ž—FBLª½«çrïCÿbƒÓ÷Ó…Bôlê݉QÆâ\ÃâtvA„BtEÒá(„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.ZOcâu÷qó©}ä$„]@ïþ.6æÞoVóÃìì²tGj“o¸³Ó½žÃW!DÇ꾡nJeú—[YõäÉXyÀÌQwÏgëâ‡ïët5†Â=»ÉίÀÓ~%õ›2ˆ3ï|Ž÷¿_Îúô­lZ½ˆO_›Å%#"ÿG)‘œóü¯lÊØÎöíÛÉH_ÏÊçòúCW2:ÚÔ̺S9kÆó|ðà ÖoÍ }ý2¾}ï9n?¥o—zÌ›7Q£Fuv1„¢[êÝgióìäƒWòAg—PÂ'2óÝ×¹1i/ß¿ÿg¡E á”K¯ç‰&3æÞ)<¼°áÑ‘˜7½ÎMÏ.Ån ¥OÊD.¿íÞÈõW¼ÀG£u‡çÞwßäæä|~úäMÛGU@{<‘AnŒÎyÉ^½ôÒK¼ùæ›\{íµdggwvq„¢[éù¡®„2üò¿ðè-g32!Wé~yþîÿ2Ý4”;¿øŒó—]ÍùÏoASã9ëÁg¸ýÔ4’â# ÂAQæÏ¼ó÷ÇyoceMø©1Œ»þÏÜ{ÍdFô A¯*"wçf>}ò^ÞËÐlØÄÀ+_æ»N )"wY«ç>Ï__]BáaÍÁŒÿ¿'¹19ƒ¯¾™ÙÛªkïÿ‰_}ÏæÍå¯ÍbñÚ™üX^óˆ^šÍ† ¨X½Œ_÷‡³àßsÑèWذÂÝ`ÝAŒù¿'¹)e'¯N™Êë[«êYôí' ŠÇ ÓàÞ+OeXƒâmKøðù§™³êÀï­9çž™ñ§ñ$W³wÓ6œ ·†¹§Ýön¿`i}M”l]Èì§þÎGé•~ýxX¼x1/½ôo½õ—_~9ôãYB! ;7¿ûÉ”vÏ<2‰Ê¹÷sí%—qÓoðuúto + 7–˜s˜yËTn¾ûYê§2ë•Yœ ÈÈ;ç0ûîc(šû7n¿q*3þ¾Ž=‰cú5Þ•:Å>áùûnâÊ˯åþwö2tÚ³Ì:#ìðþç  \üÇ~ûÿ«ôZî>ÿ×gìýôS¾úê+Þzë-BBBü¢Bôr=¾¦®DE¥”³|Íj6fÚ!3ÃÇ3 lÙkX¶r «XSȉs/à”£Ì,Í<•›®žٗ2ó?™¸%z×ëç{]Oåöe,ªý+=óE†œû%›ŠiÁF©Ó÷Me`˜ÎöM[qzY“¶}3™®68 û¾:L¡Ä Ï¥÷^€Ò_xqý!õæúuïHÏ ÑÏ…ß×>™›¯IeûðÈûÙx€U¿í!8í+n¾é4Þ¹{¶ÈÓ¹ö¾l~ùF}ooÍ¢µå ¿øTŽ«[OÄL½¢/Ë»†×”b™%rÊÂ;8{ìã,Yìò±ï÷Úk¯Ï /¼ÀôéÓý~žBôf=>ÔµõŸ0{Å™Ìzç[F|û1~ð‹2K ÕæxòrاG¡b4ŠáA,^¾óÐ&g¯¬ô?ãvîŸv6ÇŒ%ÈUŽ+Ø„¶>àðE0Œ&¨ Ãôß8ã6ìx¡öqâsyôæçYXÚh¾Ö ˜`h`>?­Íý½©]ÛÃšß ™qÊ(RÌ ÈLFª5Ÿ…ò½·r¦ÃI !ñïËØòTݽ*&‹Nul( %]ªÿ^!zšîꆗ ¬!A˜à  ·€k;ïÝrKNø×ÞxOq3×ÿs*7þ;½Éšë!›ñhxP1©€jÆ„†æÇPySÚT^~q ž¹O1ë‰t)\ñ—8Ã˲žÂ=äÚUÆ= ë««­›ÍÐÜ=yxj{L\«^`ÊÓK©Å-/þ•*·óÛNÛa¡é)ÜÃ^‡ÊqÃÒ°²ÊkK(¾I3Œšåšë°Q0ðí7ñfzßMöå- ô3f0|øp¦L™Ò‚g !DïÖ}ûÔõ"²²* }"cö×Z‡ròÄ8\Y;È­ _£ŠœåðÔÍâ¦w2jÊeŒ¶x[ió<¹Yìñôcô± >w\@ÚH†¨ëøèÕ/Y™¾ƒ™éä”7Qǵ¯ä«ï‹ˆ»àV.Ô¨`¦~œ7ý2Rª–òíâ²ú`4lûÙ‘™Iæú¹<2ó]Š&ÜÏ37å°—åXÅ· Kˆ»à.dõþº²ÓÙVÀØq¨?(Μ¸1}©Þ¾…=ZÝ2ý8þÄÁ‡o£n={¶±ËÃÐd{³³É®¿ì¦ Òÿƒ/¿ür.¼ðB¦M›FUUSã„B4Ö}kêT³òý÷Ùvî<ÿ–ΛþÊ^w,Ç]|+7 ÚËÇ_H©¦“¸r‚ÊŽmû±[ú1!5ÊJ)kE;°Qò#}'¯Þù<:^ç‡,ƒþ“Îe˜ V5ZÖ•µ=Êu\qÇ…ÎßA‰ORhSõa¿¾ôŸ{…þ÷>ioÈOéEèÑiœ|ùT®çäÇžfþAƒÃ«Ô¶ß^æÁÙÇóÁmeÊÏ×ñvVƒZ²QÉâ—ždþøxàÃÿ1üݹ,É,À®F4ü8úïý/Ïý3s>Èâ½Û^âqÇË|½SaèŸîâÖÁY¼ûØb**~böÿvñÞ-¯òO^㓵ûq&%ø÷¥‹x÷³é̾ù%^ô¼Áçëóq%0$"—/æmÀæÇ>?í´Ó¸ûæškdä»B´P7upe¾ÁÍS+¹÷î«™þÄåD¨UìÏ\ÊkÓ_`öÊš¦hsÌPμéþ< «VÁ¾­?óôsئMÏÕâQÆÏOÜÊ£ŽYÜ0ã%® w²gó~܆Ѱ»-s~<ŽG¦=Ä›SB1¹í”g³)·ÂûªK~á¯W_ǶwrÕqIL0&w ÛW~ÇÓ×½ÎÇë63AN5[Þz‚ÎyÛî¿€ù·AaƒF½ðþrYn»…Ë.ŸÅy}C09+(ÌÞÄâ÷Pq°ñå[¸Íñ÷N{ž c xÛþ=ýïÌÞ\]¿M¯LåæÒ™Üuõƒ¼v{8&g9¹kY˜]QÛÏncÕ?näöÒû¹ó’‡yåΰ°ý‡çøñkü õ{éÓ§³{÷nß !„8„·ª£ÒèvËÚàbjp1–Úkkíí†×¯ÆÇÇá³@º®£ë:Æ cóæÍ­|YÇ”:Ͼº„USÎáý~ç‹™aw~Îg×à…›îåÝ­MN‡5jÛ¶mCUUTµûö0 !z†òòr 635ÃÀ^kµ·5ÀÓà¢7¸.u©.uëšzÇ33ä¬k­ì!§°OÄθùïûœÇ3Û*Ð4¶½óþ>ø%f~º„ë·­ã·ïgóøìUTÈðq!„MPo %”ÄcÏäÆóÒHˆ Aµ²sõ\î}è_lð>¬¼õª2ùðîóø&å8N˜p,J ©’@BÑ õ–0ÊXükXüŽÚ ‡Ê=kY°gmGmP!D7&ŽB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêIíÇîÿ'_:°æ¬¯ÿB!Ž€„zGR£5ù Æ$՜߶ñßB!ÄèÖ¡xæK¬Ïø…ÇN õò¨™ÁÓæ²yë‡\ŸØ­_¦Bá—nœv*Ññ±˜ûqÉÌëÚè|sJìyÜ3mJ4±ÑÝøe !„~êÆi§× öŸ<…é§G6h¶2ê†éœ\±‡=ŠèèºGL ¼ò5~X¹žôŒ 6¬ø†7ï>•¾uÚjfOÌIœr”?UeÖßôsm+~a­{§Uó# pcFèlXµ{«ö‘BˆÞ¤û†ºNDØ*lhù_3ç[soü#ýÂNâº+’XõÎÿÈpVQiƒ°¨ðÚš²•þgÜÍ+ŸüÀÒ5ëX»ä?\›lÂj hr3ž¼öé‘DGø³«Z¾þ†ŒÒÅ|·J쥀yè8Fe°ly †_kBÑ›ußPW»½ °³êÝÈ:özî{`g»çñöü"t£Š*»BxX( `J›ÊË/N!a˘5õ*®¹õ~Øß|\ *&0 <0›Í^›Ó[³þC7VÊâo–¡M8—ÓcÌ <þâw-á×½;F!DoÕ}‡_)!„‡)TW90ÏîÏyï×[xî’$¶<ÿkÕØŠ XÒF2D]Ç__ý’•å¨6rÊ[˜F Å¥ ‰©É}X“x€ŸëWT¥‰¿ ÊÂ÷opñ…ÇyÚ@²~ü‰=ÿ‹(„¢÷êÖ¡ v{M¨c”ðãë/0º,‚Ÿ>ÝMMº°;ܘÂà V 2k;{”ë¸âŽ )œ¿ƒ-ž¤ÐLûâÙÍâE»¸cÚL˰2o{9$ %ª¶½ÃåkýzËúM8—“RrX’Ûèï=6 Ç>þ<—O§>Iÿ°,Þ°îµ!„Í벡®ë:f³MkbØ·DH 8ŽúþfgÆÇ<þpÃ… v'Jb¡ ”fÎáÏÇñÈ´‡xsJ(&·òâl6åVøY*Ì·îá/‘2ãöç8/Ü„³ây›V‘Y¬£íò±~=¯ÿõ“»‚Y—/â×g76ú{Û?y7<Áñ›çðín‰ô–0™LèºtW!z'oÕT¥Ñ통ÁÅÔàb,µ×ÖÚÛ ¯_?&""Âgt]G×uRSSÙ½{7ÕÕÕ­|iÝXÀq<ðõë$ýë<îüú  ’kÀÀ@RRRÈÎÎFUUTµûBô ååålf.ÀÝèZ«½­ž½ÁÅht©sHDtÙšºËå"((¨…z‰ÃF£¦<ÄEUïsÃ÷è-ŒÛíîìb!D§èr¡®( Š¢`³Ù £´´´³‹Ô1Ì)\øä»Ü9ÔÃÞµŸñàÿ&C²©Å°Ùlõï#!„èMº\¨×©¨¨`РA˜L&<ž^Я¬eòú%Çñzg—£3™L„††ràÀÎ.ŠBtŠ.Ùá¨( ‡ÊÊJ:»8¢›HHH ²²MÓ¤–.„蕺\¨×5›*ŠBQQ„†ú5ñºèÅBCC‰ˆˆ ¨¨è÷Bô&].Ôë(Š‚®ëìß¿ŸàßT«¢÷ `À€äåå¡ëº„¹¢×ê’¡Þ°¦e³Ù(..&55h=7å IDATUjìâ0!!!¤¦¦R\\LUU•ÔÒ…½Z—(5᮪*¥¥¥¸Ýn’““)++£   w žM2™L$$$A^^UUU¨ª*a.„èÕºl¨7ürVU›ÍFVVqqq 6 ›ÍFEE‡MÓšžyNôf³³ÙLpp0aaa„††RYYIVV§>Ð¥–.„èͺl¨ÃáÁ^×Çn6› '::‹Å‚Ùl–™Ãz8]×Ñ4 ·ÛMee%………hšV?kœºBtñP‡ßƒ]Q è@WZZJII †!s®õ& ÃÛd2äèBˆÞ®Ë‡:úe]îua.¡Þ»4 p s!„8T·õ:ýŽ{ïÐ8¼%Ì…âPÝ*ÔëÈ—»Bq8]&„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.„BôêB!D!¡.þ¿{InÉ(š¶kÿ+ë…ÔÔó¶Õ›m…ć¤,ÕÕ9€.þT“«—@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@„¨@ÄkDýeµ~yµNšÔ â™Q7™ÀuOëç#Qßû/“cø¨Î¶ñîn>:©}° À/¯ÚÍ{¢~öMëðÃ=M¼ÜΫQßú€—ñû•|[ࣻ§——zïòû™vkü2ëâÃ1¿yö-m/“ >ºWoä£W¿o-;ün¯O;m}&êG°uòÿöšï¯€bÙÅ­¥ø£Ööôʤ~æÃ¶¶ï>ª–A¿²¢}z8¾÷–¶£¥vËïðËÕ^ÞÕέ¨_½n¶äþ}±ÀG·ìâÞRüÝ÷±?zŸúlb_‡Ý¤ÀG¶nâÖRüóΞrõÙ½sg¶Û—ÿvå @Ô·ñ{Øï9u½ù¿žð%·B¾Þ^ÆãëׯOø8ø×9jäS®?»7êg§ôÛ¤þò÷ßÿgŒñic«ã±s oiv Ú™‹È—“ú™iý²Y0·¢º ñçŸÛ§1Æ—ŸÇ_VÛ_ûåöy±ÿ´Ø¯Ãô½àY¶>»~ìÛb¿Üþ{°_nÿ„Ç|ŠŸýR1Æx|Rÿ´úá–!þ¶Øñ{ˆ×ÿS>ÿ|Þò—Qà-E}¶¤¾ŽõV¸gKðc<0©?rN}oé}öÙkn?ÈçÕ¶·D?6Žà5œ‰úVØgSû2ìGKðw9õÛD¾ü¡Æø}J_}ö­÷{?~ÛÏ–Ý÷εÀŸ°uî|¶ ¿Žú:î{÷ªÏÎËo}M³¨/C>û·½¥ˆ±ñúÛs¿Œ_1_NêGçÒ€·²>§½õÙÔ¾µô~fRߊùfàïY~ŸMë{9îöœeз–Ü—Çcc¿>€×4[‚¿í×Óö^ØggçÔ//ßúò¢¸­iý6qoÅ}ýÛÇ:æ³%÷Ù„.êü)³©yﺲYÜ÷îQ_‡}ëó=z¡ÜÍ-ø·¸…ýö¼YÔÇÆ~LÀk9Šì^Ô×qŸ=~xB¿ydù}yñÜ-ÒcüöõEqË‹é®LèBÀ[›-ÅïMì[ß{ÎX½ï%{QßZjßZzߺ2~îÛ].߯¯7¥ð^]Ö÷¿õø(æg.œc\ŸÔ×qž]‘· ú2Ø/ãÜtnJà=:sáÜ™m¶ì¾õž§=zN}ö1æ“øÖŸûõ1¼gþ~¼µÍ^s—3ÑÜzÎVx·‚½<~4æÀ[™­L¯ÏÄ}Ÿý̸7êëŸz/ô[û³ŸÒ,¦Ga¿í÷~6è{ÿ>Æ8Ê+a__¸ ð^] ûòøjÄï ú×by6¸gÃ}öÊvAà½8ܳá>{eû©óìWƒ¹÷ü½HïMãWÞÞÚ^`÷"½7_yÏ©{¢y5Â&rê®Nðg^{ôßþáÞ>ë»GÞÞÒ™à>²œ~ùÖ¶GâùŒóñ¼'¼gã{%ÒwÝ«þ¬ˆþ©_à=»ûÇ<øÚ1ÆëU¤àœ‡C¾ô',òðÃS#¾öùø)ðïõ?ü£fx‰gùÊIEND®B`‚OTPClient-3.2.1/data/screenshots/emptymain.png000066400000000000000000000322361452112020400212750ustar00rootroot00000000000000‰PNG  IHDRõN®­ž:sBIT|dˆtEXtSoftwaregnome-screenshotï¿>'tEXtCreation Timeven 10 feb 2023, 16:32:16¶à±_ IDATxœíÝy˜[gaïñß9Úf_íñØ3öxwìxÉâ•$—Ø ÙII $„BZh ´¥ÚÛ^èBKá!šroÛ{BiZÖÐl$nš²Û΂cÇN¼;ã}‹gŸÑ~îã±eYË‘Fi^?Ï£GòH£ój¬£¯Î&I «ÔHPNc N© •>¤¥ž>…V²À—"ª„p¾ÓÀE`G3 ÞÊÍhB]ÔÈ+šnï7—éx@©äc··-xà J7÷—î6£ù]ŠÅM|ÓÝf4¿›³BF2Ó}¥ºn4? Rx4?ssk…f.1Ïôïl·ÍeºŒF¶È&_︼.Û}*îÅÚ‰ÍM¼ÓÝm܉: XÜ®NOs'Ëõ™¦‘íº´Šõl±Nwžîgn§@!äôL1Ï÷Ñl‹?G¾QÌ%è™ÎÝÜ&—i0n"›-ÜŽËÛä2MWò b¶ÙÜF<Õ¿3ý¾›i0ÙvpsòTÿÎôûn¦U®AÌ'èébžé:%]N7Š%Ó*ólO>¥»m¦édúYJ^·7TnAÏq7§T÷•m<B¦íçÙž)쩦c%\o¥¸mªŸ¥”KÔ3É5âv–Ÿ%ßWâ4’/gúnä²Ú=UØã:7â©~–*òÉaϛۨgЍ› Û).§ú™›%÷Lc ²=ùdëìˆÇ5Ü©äŸešž¥ôwý|—ÔÝ=9àv†ËÙâžjº©Æ@®2-©g‹z<Åyb ãI÷‘.îÙž•›¨§Û–KГO©~ž)îJq9Û8p+[ÔS…=UÌG‚žç‘°ëÔ¹­üž5ô£Ý¦ž¼:<1è©âíQêÈ·IZ&i‘¤&IÍ’i¦ãf,äÂÍRqâmB’Þ•tRÒfI¯J:¤3aO |ºû‰§øÙ¨¶­çËLKééVµ{2\ž%鎪ªªy&L¨¯®®žêõz+=ϲh4 <9Ž£X,‰F£CûOœ8Ñ388¸MÒ$í‘Ó™°§»œji?ŸCÞNËVÎL«Þݬn÷¤9÷Kúxeeåšöööé•••Sb±˜"‘ˆÇQ<—ãŒz'@ŠÂ²,Ù¶-˲äóùäõz544t|ÿþý;‡††~!éIaòäóTqOw|{¢´ôdwŠËÉaÏ´íÜ£³ƒî‘T'é[[[/›:uêrÇqꆆ†F‰9`ÜYF£ ‡Ãòz½Õ'Nl·m»º¿¿ޤ’" ¿âftò6t·¿')sÔÝ.¥g[BO<$}qÚ´iË— Y±XÌíX([±XLÑhÔª¯¯ŸRQQáïéé™&i½2ï—´‘Ï5êÙ–ÒÓÅÜ£áòîlmm]ÑÐÐpñÐÐPî€2‰DT]]Ýâñxâýýý•’ÞÌò+™¶¡ç´ƒ™Û¨gÛ–žj•{rÐgWVVþV[[ÛŠ¡¡!ö‚+ª®®nr¿/n–Ô­ô‡Í¹ÙÎU7GsH[ºÕðɱ ûG¦N:# ¦X$Q,S<žéCwPl¶mËëõÊë-Ô§Ãc=æû</ã„ù‚Á ÕÖÖ6}×®]·KúºÎ=Æ=±—‰Ç´Ky®šÏ÷ ]2m_OµÔÞVUU5ײ¬ÖT;ÂY–¥p8,ŸÏ§¦¦&•â¶ÎÎNuttŒùtS)ÕXÇQ(Roo¯"‘ˆü~?;/žçÇQ8–ÇãQuuµ¼^oÑçOÇqF … …ä÷û³N³¯#Ì/ÈÄqy½Þ)UUUó§H:¨³?V61ò‰-uó%/)¥‹zº9!Õvõla·%-›0aBC$9ç%)«ººZõõõnÆŒ"²,Kª¨¨POOäóùJ=,”P8–ßïWeeå˜Msä0!ŸÏ§`0¨P(¤@ ñwJñ:Âü‚l"‘ˆš››ë—I:¬³Û˜¼Äž*ìRê § ½íbLÙ>’ÕÍžðz½Þ)©î<‰Èçóô2T__/¯×«h4Zê¡ D"‘ˆ<Ϙ=YEE…<OÆça9¼Ž0¿ ŸÏ7EÒ…:÷SUÓ},º”½½)¹‰z:é–ÚS-©OT•êNb±˜jkkG1 S]]/Rç±X,–u y,ŒÏÃrya~AÕî`ºïBIÕÓ¼ä³gGº¥ôL«á,Ëò¦ÚÖä8Žü~ÃÀX`áùíÔ6ÁRC^¯7ãó°\^GÆz~±m[~¿_¶mËã>˜id'Áp8\òŽß0˲¼’•>æÉ§Ó¿ªw˜ËåûÔÓ½“p³]= 4ï<Ç‘mf…ŠÉ¶m¢~s§$;­&³,+kÔËáud¬æŸÏ§ªªª¬odÂá°•n¦ba|ç°4ÜÁLK驺š¸]]*â÷©'4Ó¡m§N`t,ËRmm­ëM"~¿_~¿_¡PH}}}EÃÁøR;õ{g5Qçö2Õ’zÎò]ýžjA¦%uÀ(x<ÕÕÕåµ9$Èãñ¨··WÅúhnÆçJ¶%õÉ{Á»–íÑe{Çàæ·Q¿ó€;vµ#ÿ²!ùfFÔw?G ´¦OŸ®Å‹«¥¥E’tìØ1mÚ´I%ÙøcYVÞAáõzUWW§îîî‚/3>wÃÈr’Üõ2cès}„év·Ï¶ÃŠÈò9 ,)piPò:ùEFÏnÖ¢k®Öò¹ Ú÷Ð}zjÿùù…E+W®TGG‡6lØ ƒJ’ÚÛÛµjÕ*Mš4I¯¼òJ‰G8¾ÔÖÖd‡E¯×«ÚÚZõöö`Tg0>×rÙA.ñ²ëWõ|eºw%YRöÙguå•WseËÓUÕõ²ëöÒ´¤ú/t{㘥xPŠò)´! Ø±ÒïÕlïbÝñ—_ÖÍUôýç¿«§öKª^¦ÏÝ÷mÝѲUÿðéß×ì,Ü!OvE£Ú¦·(Ú¹]‡Ëä;’¦OŸ®éӧ롇R(:ýó½{÷êСCºå–[tôèQ–Ø]òù|=¬0Èçólç4ÆçšÛ˜'ö5g£=¤-y éVÅ£xÙÕ’=',ḭ̈~R«Øáñv«v®®ûÍOê¶÷­ÐüöyÃ]:ðöz=ùÃûôý§vkÀ©ÕÍÿû9}muú:ü—µú^ÓµßþýáªÉª¯´åÄÂì>ª½[ÖëÉ}W?|þ€‚§§9G×~ìúð5+´`j³ª4¨cïlцÿ~@ÿxßó:˜bžÖ‹tÙ‚FÕx–ê²ÕúÁΞ‚¬L±ênÖ½Ïü­Öø¶êÛ·Ü¡ûö”ÇZÅ‹kýúõg}D(Ò† ´dÉ’’GýÙgŸuu»R/0TU¥üˆQ©®®VwwwAîk<Ï¶í´‡­eºnãË´Ê=][G½M=]„³ý<Õ9Q/²ØA¯ú¿_'ÿÒ Kƒ²N}BeÏ·Ͻ±×‘§Î‘ÿÒ!ù…Uñž ~V3¶.{Âj}éþ{ôáY•gž`Í^~³f/»ZWýÛçôÉ{ÞT<Q8<| Ûã“×cɉE‰ ϬáHLŽ*Ô4¥U •¶bÁ~õ‡=ªjœªE礪—_¥K¾|»>÷È!iÂ}éþoœ=MÕiò«tcd½î»ïù”cíù©¾úÕ]7a›~öTa‚.I²ly=å7{Mœ8ñô*÷T<¨5kÖŒáˆR+u¬Ý9ŽºÐ|>_Æh¹•m|ÙÞ8¥û?‹ñÙ¶­{î¹GÏ<óŒüñ³®»ñƵfÍ}ñ‹_L9†QŒ/UÐS§ú=Wëf1-ÓD²…œ¨'b)´®RáM–å››fµPÔR줥àsUò/ ËÓ:N?ùÊjÐÕò—úЬJ9}[ôÿýª¾÷ì Ô_ k?ý%}ñ׿è‚ßø+}î¥[ôWŸ[¡ŸK’üZóõôï¯ÖÐߥËî~Zá‘ûóÌ=u!¦·þñ#úèw÷Êj\¢O~ëÿê–7ë|ôFÍxüAÍþŸ§¦Ù¿MÞóu=ðüÖhʼKu6¨3¦Ô_fì»X·ᓺ¹ê€œ—ŸÒ7ÞˆJž­üÍ/è³¾Ró[<êÛ¿QOÞÿ-ÝûÈ6 8’<ótÛßü±>¸xºZ›ëU[á¨ïÀ›úÅ÷ïÑß=ø¶úgcïB}þ±Íú¼¤Èk_Óõ¿õï:\ÂÏôȶjräKW]1?PÇï÷+ f¿a–ûÈ&]¸³¿Øã‹ÇãúÅ/~¡»ï¾[^¯W<òˆ$é†nÐ]wÝ¥{ï½7c´ó_¦%t7Ý̺äžË‡Ï䊘1gÐVð¹*ŸËr»SK®V`|îQgMxŸ>´¦Y¶3¤W¿s—¾öèÅ%©÷5ýä/þDÍüD¿Ád]ëeúæKOª?iD»ÞÔãÏîÒg–_,{B‹&L¸Z\Ý$Û êÕï|N_yðÔ4Õ£Þuµ-§{¯Ö²/ü‹þéÎÙò…»uðhPMÓߣþõ<5‡~]w¯=!Ç3E­^¡5ŽBý'Õ=X£æé+ô¡/Gñƒ7ë+/ž¹;'¨®ƒ‡Õ‘¢‡{U+áQ#ŸrV …ø°žñ>¾'žxB’t÷ÝwKþ»ï¾[÷Þ{ïéÈi|ù6µè>“<ÁäË_RO÷î.ÕÏÇÃêµÑªû½nÉ‘zÿ©¡ÔC3žŽÙšî³¤Ø½´î°Îz/Ý­—×Òg.˜¦Êé35Å#íȵrž 5Íxî¸až¼’bGéÄ´‘iîÕºõIÓÌ‘=é&}ê¶Yò…7éï?ü[ºoWT­ú?zä/.ך_£–' £#7ŽÔ>}“¾ñæ$ÝñÿÖŸ­œ¤Õkék/oÐéõ,±]ú×Ï”Ç6õOúÓgg»í?ÿó?{Hi‡mêÅü”¼BÙ„ñ=ñIJ,KwÝu—$é›ßüæ9«ã 8¾t;¬“ùÒ–io½¢8Õ VʽßKýÂ`Uäù GCãxEŠezB9>zÏ«E_ø¹6áÌOœØQ=õokì›–wþ]Xaɲ–èóüJŸO¸.6¹]“m‰úé+ŽhÓ›G_9Sõå•T®O܆ÚMø‹é|xÓ²’mÁ7ÝamcrH›[ã¸îŒ×†ñ¼dïܣΨ£ÉþYzÏÊVÝ¿çà™%gïL­Z1E¶ ìÛ«Ã9-¼:Ã;Ö…Bê>¬Ý[ÖëÉü«~úê1ÅZ÷ëPÜQ›wºV,Ÿ¤Ùu(ÿ¥uÛþ"åà6=ùïÏ o‹A÷¯t$®”Ÿî ŸÚ Àö ÏXÎÈ›[¶ñsÚù©˜_xRˆOn3a|‰ÛÐGV¿'nc/ÂøŠ:·ŽÅñL¼Ü‘¶dùY~çô¶òTj§G‘>…^­38¾?(~üi=ôÂgµüê-ûì=úãî¿Ñ÷^èT°~®®ùÔŸë“xeÅŽê©G^Êq{zL[ï½UýîÞs·K{NO¿ñy-]V©eø-ýéà7ôï/ìÔ»a¿§]¨K&ÑÚgv(ìÄ‹;’U¯ÖÖjY:wo÷è®mÚ½Iùšä?ü_ºÿ';Ô—ümjÒ1—¯“N¨OýaGªš¬93«eíî•åõJÑè¨6ÚÈy)WµWÙ‘ïÞåRa‚\¬t•Æf|7ÜpCÊmè#ÛØ3…=Ïñ½‡…øêÕl·AÅ{å툨âÊA…^«Ógˉœûç·kã \’qH¡×*~­"åíÆç„Ö~í¯uÙ¼¯éýS—èc÷üT;ëú vÿì+úös}…[=? ¿þ­ùîÝZÞ°Hw|õÝ‘xõ‰‡Ô½þKz.Ô©ÝïDå,©Ó5_{H¼IùrÒ]íD÷=öýý-íZó¥Ÿé…?êV¿S©úšý½ÕúÒ a¹Ù¢ túþžÿÖÒ!U>¦OÝúwÚT®ëæËH©7¹g~.ŒflÙî»P÷áöïœÏ}æ>lÛÖUW]uÎ6ô‘mìkÖ¬Ñc=–6ÞŸ›~ä8u·ÆiÌ|¹B5íQù†å_˜ýIfù¤ŠUAù/ kpmµbÇç‡ÏÄŽü—þ×mûµþ“ŸÐ×,Ó¼¶zùÂ]Úÿö+zúÁûõ½Ç·©¯À;÷·}_Ÿ¹m·îøíé¦Ë©£¥F¾è€ÞÝ¿]_z]Ç=–ëÔ¿òUMÿ³ßÖûæ‡Õ}âÜ`‘Ó¥_þÕoê÷:ÿP¿û˵`j½ê£ý:ºc‹‡²v7Ç鑯ü©¦ÿùéK;ÔÜ,9’¿œ7¸—‘ñ°élä{½ }h[$)È’p¶ñåû7‹ñÅãñ´Ç¡?þøãZ»vmÚ1j|äÝ×tŸ`“x9ñ”øí2ž„“÷ÔÉwêäO8÷KúÎÌ™3—¤Ààà :::òAuvvŽË±xZ¢ ,Ê3%*»R’'{â½¶Ÿ¨VìPö¨wvvåS£PþÕÔÔäúö¹ìÑžëÞï'OžLû<,·×‘BÎ/^¯W)>LjºººæãsgÏž=›$}VRøÔ)’pÑð[ñ¨¤XÂ)žpr’N#rþð·Xr/‘Ø1¯žù“áê?ß%YÃTþU…B¯Æïêwà<F … öùå¡P¨`Á”_”ì67(EŠ÷zÙåUøµJÅø/Bñ”úP5õõõÉãñŒú›Æ¢Ñ¨úúú 4ª3_Þ þb\Ì ª”£Œô}·®ÔCÀy€=Ü‹ÃqõööŽê;Á£Ñ¨z{{ þ]åãËCÑú8¾m€óD,SwwwÊo¿Ë& ©»»»¨‡ 1¾ò0>w}€óÐȧ×ëUMMMÖ/ƉD"êïï/Æ6`ÆW¦Ju˲Ç‹úÂÈ_<—e±%å|eY–Ç)ùs ÛÊåud¬æ—h4ªîîîÓ_-jÛöéÏ"Åb§å*òaWŒ¯ •EÔÃá°***J=¤‡KþB‰Ò±,KÑh´ä_•F3>Ëåud¬ç—x<>ê¯'-&Æ7öJþjíñxÔÛÛ[êa ÞÞÞ¢~½"Ê›ÇãÉkd¡ƒÁŒÏÃrya~A©•<ê>ŸOÑhT===¥ ’ôôô(Žú0Œ_>ŸO±X¬¤K3Á`Pñx<ãó°^G˜_PÊâÙç÷û500 P(¤ºººÓÛ80öF¶%õöö*Êï÷åŒ~¿ÿôny½Þ¢o7vGÑhôtÐÝ|Lj)^G˜_PnÊ"êŽãœ~§}âÄ Åãñ’Í%™n*¥‹eY§wñù|¼@A–e)(jpppLæÏÄç¡ÛO+Åëó ÊMYD}„×ëeÕP¦ÆËü9^Æ ë¸0QÀDCu AÔ0Ęí":888V“à¼Ä’:†Óƒ9ëëëÇrrãóù‰DJ=Œ²dú߯ôÇWJümq¾Ë/fICu AÔ0QÀ|ëɪTËÌjkªTÏŽÚÓ/õˆ #ÏÇeU4©mÊD5ø{µ{ûa ñåk'XRÇY,o¥ê››UÃÛ½ó‹Ý¢…ï½B+.ž«I•¥L¥z\¾ÉZqËúÄoܨ…M©_=íKuý5«õž…mª´Rÿó ÊQY<ç]ÿ;ZÝáI{}lß³z`í6ËüݲU9W×Þ¾ZÓŽúß^«?·_ãé€+0W×~lµ¦Ûǵá'kc÷(—Öì&-½õƒZ:A:¶á'zxc· Yþ;‹åoÒ¬%ë™mj®«R߉Ú½e“6í=©ˆмë?îâ9~H3¯½IËÛkðZ’W$8 îc´{ëÚº¯Wѳ&\¡Ù×Ü¡«fø¥“¯ég?}M'æ»ñRÝú¡eš` è­'~¨ç÷G“'[¶f_û‰¢=.»¦Uí+å·&kê¿¶ž *ÛKË9¿30­°ó P e±¤Ç‹ ŸâñáÙˉÇOÿ,3Œ¥¦ kšß’d«zöÍ©±J=¨Y²­ñ6æÒ²*;tÅnÕU—ÎÑäÆ*ù=¶¼þj5N™§¥ï»E¿¶²]–ãò9îUUM*¼–œhDáp\ÞŠZµt,ÐÊënÑÕsktÖÿŽÔþûr$«q–f4'^k«aÖ 5ÙR¼¯v?A—TÔÇï~[/¾¸Q›_Vë÷dzªßa^A¹*‹%õOÞ¯’$¦¯¹S×Îõ)¶ç)}ï齊Y•šwÝGuç§Vkð­Çõƒç(&KÍK?¨.mRlÿóúµÇ4ãÊUš?©QuÕ•òY v֞ͯêµï*<2×ZÕj¿h…–ÎïÐÄj[ឣڽi6ìxW‘Ñ®ðMÕÂùͲâ'´ÿHµÚÛÚ´p~³¶½zâÌÒ©PËüeZ¶p¦Z*d…útt˳ZûúaE3]gWiÊÂZº°C-Õ^EúŽhïæWµáí£Ã/|yºþã«5Í:vj©AšrùºiaµBo?¡žÛ¯¸Õ¬«Wiþ¤ÕTT(à“B½GõÎæuZ÷ö‰3#I²[´âößÕ IñC/齩þ‚­%±TÓ±T—-»@mM•²#A tíÖËO¾¬Î!¥½n_|nöÇ(eÿ?¶ë5kÅr]Ø>IMõU XQ žÜª§ڠù¾w´*4ã²+5¿É+…iË‹/éÎ.E+&hæ%—ë=s›4aÑ•Z¾ÿ'z>Ósüôß½éÔG'^ÿO=¼±[ªœ¤‹Þwƒ–M©Ô´EsÔ°k£ºÆÚ¿SÁYšWÙ Y³&êõÇNýš4kf³l9êÙ½CGc>µ/¿^—ÍmRM•_vtH=G÷èu´ãdê0ºz^¹ù›ç¡˜Kö$]¸ò"ÍõöÊ9°GëŽÄeZ´`åJ-ž9Iµžzú­³ß@%ýÎú®‘Ÿ'Í+?ߥ¶k?ª+;M^u“®_>W“ª ôGämhÓÂ+oÒ•³F»ÑRåÌšU%EmÕ ¯íT_ÜRü Õvúm“W“–Þ¤›¯X¨©²"!E}5 8AÅ3]gù5eåMºqÕßL`UÎЂé²œ¨¿ò´^ÚqTý¡°‚=‡ôÖs¿ÐïÆåØÕš=ªüyÜ¿$ŇŽiWg×ðÿAeµÎy¦FhÛ®>Åe©næ,µœš£í‰³4«Ñ’œ“Ú¹ó¸âŠ)î¯Qµ'ª¡¾…íJ5N]¨+ßw‰&ŽêU HóÕX>.«FsVß ËçOQ/®PÈVm]…»ÇäyÅÅëPLe±¤ž™£Á}:oÓ”†vµ7¼ªƒ“Õ6Ñ–œcÚ·oPŽšOÝ´W[×þXëŽúÔºü&½ÿ¢‰j½x±ÚßzVû+æèÒ ²ãGõʃjc—£š×éÃWLÕô³¤][ò¢]«¹ó§Ê§°önß«¾#'´«{‘.iœ©3^ÑCRõ\-]ñc­;âȶ-å³…ÇjhRƒmIN—öè?ûÍO¼Kûõë’ uòÖ7ªÆ–Næ8 Çöª²¾]Κ K’3دsnÓѻԳð5ÖÎÒìÖWtä4iö,ÕYŽbGvhÇIG’£Ã/ÿHß{ÉV ªB¾Ê™zïV©½~²&W[:žç÷-eû›ç?_Ýã²ëfëÂiYN¶>ñŸzñ@Hþy×ëã«;”u{ŠyÅÊòzÓ8ˆºäôïÕž£+4yr³fL«ÕÖž©šìµäGïôÅuΜç„ttëv]2QS4±ÎÖÁÚVMôX²¬IZqÛïhEÂÍãÕµ£ŸÕ4Wó&y¤àníÜ”ㄵk÷»ºhY³¦^0SÕ»¶jhB«Z¼–œà;Ú²í¤¢Ž$EŽJž¶ ×5·h¢Ç’ܧm{ú—Ô³c§¯j×tßDµ4ZÚÛÇ ~=Þ/µ7¨¢ªR¶tf5p9]û´¯{±ÛuÅíÕÜ=Ûô曵çd8ãunØÍ™ÿk,‰ú™ÞÆûƒyâeùýœ7½ÚjYq»>•ð§_{6ïToŠ7ñÛ´íè­l­ÖŒ9S´þ˜£¹3kd;u¾µC}Ž$»FÓ–]©Ëµ©Æ›°j9î“Ï›ÿ¶álóÑ«Çe56©Ñ²ä ÔÞCÃk¦b±ü熬¯W@‹¨ËÐÞ‡µbr»&Ìœ­9'ÛP\Ç÷î~‘Kµ:WÜ‘d[²,G²†_[è»Úýf§z^‡àáQ ΣֹsÕ`ÚûýsϺ֚|æ4¼­ÍÖÈk»#'¹™®sÁ‘3œË#ǑۊÄ^¸NOÿì^ô°6<ú¨N.¹H‹çuhÒÜ¥š4³C¯?ò°^;žáº^1Ëÿq¿û?+No—ºG5v“ÚÛ«õFwß™¼Û jŸR#KŽ"=ÝêÏéµÜ‘ã ïD ö«ëØíÙ²YoHýöÁéïn~,ñIDATÓŽ·öëÒIÓU5óÍ=îhFµ-gh¶î’#Éß±B«/jW |T[×oÕ‘È]xù"Mö(íßÄÕóªhóUñWZ¶-Ûõïd˜W²½^E4>¢.Gƒ{ÞVçŠvÍjY¢M>)~X;w÷$"å‘ÇÞû¼fF‡&Ú’éVWŸ£xä„N:sÔjWÈÛ¿Ko¼uRaGòTÖª"zîò›kÞ)š3«V¶Wh WC§÷ͱ¨©U¥w‚æÌiÖæíïªËq4)Сùsêtx{¯b²å÷ÛŠt¥¿.úî1ÍU{`š.˜Y£C»†T;wŽ&Û–œð ïv¤ÈÃŽTQ§I“ªe¿›ß*>'Q$æH¾57údu…$Û–âñÂmW· 8ǵ}ý“ÚþZ³.}ÿ¯kÙ¤fMŸV§_½;˜þºÙcüdæÿãé îÑö}ËÔ6£BS–½O«‚/hó¾nE*š5ë’ÿ¡‹›mYñ~íÝžë¡ŽŽ¿òÓt4´w«v-Ÿ¦Õ3´j•#¯å¨{û[:‘$[•õõò[Rüøvýjë Zƒš²òTü$ÉqäÄ%Yª©ñÉRHŽ‹çU¶¿ùèéq¥x ïÆçhrE‡æÏ©×ÁmÙ¿Q+ó¼âöõ (¼quÉ íÓÖ}š¹¨N~Ÿ£È¾ÚÓ—4{ÚÕºð¦kÆ£@u@^ÅÕ³c«ö…%'²Sw,Ôµóê4ýòéÎA…Ÿ*üQíXû€~¹/¿Õmþ©s4£Ê’íÔË?{RÛOô”­–•Ö-5¨aölM|ý ½±k‘®™S£YWÞ®Ž÷„±üò~Nü׎ ×íÔo/Ò”… š}Õê¸"&ß'[Qxs“:C’tT{ßмù5j¿üv}ì’슪ÜJãÇtðHD³¦UjÖÕwhò`\Þðv=þàz+Ы‘=áÝò åëëUÄ£Ú&KrÂêë’•á:'>õ1:}Åù?NËÒî—^ÐÔ¦5šSߢÅWÿºŸu}T]Û^Ðú}…ÝÙ0¥Èmyû¤.X:A>ŸäDhËÖã§"×À‰ãŒ·¨ºm•n¹e–zÂ5ï G]½q9-Í\ý!]ù‘žïÌþ¼*úß¼ëÀÙ“púvhãö…š4¿N3Þ{›î\1¨¨]%[6¬d™W\½^EPþ{¿ŸÕÑ-oéxÜ‘œ Þy{“ç'¦Þ ì ¿êÑ-ÏhíºCÃKIÎ:ŸTO¼²]‡z‚r¼Uxbx÷¸úb®÷!?Ç´9ª°EîQçYŸŽ×ñÝ{ÕåHvÍ,ÍihïséÉ×wéhoXòäw†ÔtäÓP†ëÂ:ðòÏõÄ+»t¬?"Ûg)ÜwHÛ×=®Ç_?vêÃHÂÚ¿îI½øöaõ†¥@U¥<±!õž8¨}ÇrXZr´ýùg´ißIã>UUÚ ‡còðYbÛAuwe×4hBsµÔL»^yJ/î ÊÊpãæ1fù?.Æ…xÿnýò¡‡õË»u¸{H‘X\±ð ºïÒ¯žyX¿Ð9F1ê¨ë­Íê ï<6¸{³v&‡=øŠžzy»ŽôÅT9qŠÚÚå õêø¡C:r¤x¶>÷¢¶îQØ +4“«çU‘æ«â>®äIµïÅGµöÕ:Ü–¨R…'¢Áîc:xè]¥<ø"ë¼ââõ (‚T¯sVÒåÄ“pò$œ¼’|§Îý§.'ž§µµuI}}}~ƒ´,9ŽTÙq¥n½v®jz7ë៮ÓÑ‘ùÓÓ¡5w^§¹Þ^mzôÇZw¤°+¹|>Ÿ"‘ñôÙpcÇô¿é¯”LýÛf}½Ây§§§GGŽÙ$é³’Â’"IçÑS—£ÞgyäO89I§g½]«ß}šyÕGty«%ou…| j÷k›tŒ@Ùáõ ¥UþQ·«Tá‹ËSY%+xR{6>¯çw%äŠ×+”ظXý^j¦®&,Óÿ6¦?¾Râo‹óÅX®~G;Ê€Lˆ:† êbLw”óù|ÙoT¦ÆóØ‹Íô¿é¯”øÛ…5¦Qg§ЇÕ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!ˆ:† ꂨ`¢€!Šu'éœQ´N²¤€! u–ÌÈ]Áú9š¨g„“æ2ç+·mÌ»›£]RÏ6a‚ÀEíf>Qw;A–Ö–Osng®QO5GgïÉ—êßœïòéeN Íwõ»› shg¤ëâ¨c>¢Ð‡´9iNœïŠÞÈÑîýžjµaàl™ÚX°ÍÖn¢žm©6þüN<Ëïp¾HìbªUñÙZ›µ§¹,©»™XªS<‡i`ªÄ ç²FÛõÂq¾‡´e[ÕÎêwÎȵ—yµ3UÔs=~.Ý*÷x €ó]b3­ŠÏû8öѧžn‰=9ì,©ÎgÉMLµ*^JßYW²E=ݱsnN#ƒå2 ÓÙaÏgÓuÆÈ{ 0ÈT!O>9’ÔÓÓS€É0îdkdAö?Ë7ên—ÒG–Ô#G޼&ÉJqRÒee¸ @)¥;ÍÍNä‰Kên–Ös–.˜©¢šbûÔÉ’ä9uÙ“tòf9O<Ù çVÂyrø³€BIðtûÅÎOÑ,ç‰§Ó ÂJ¿ŸîM…¤Ñ/©[I.1ı„séì'ÿQìS·K|³@Ô¥”-êéV©'Ç:U¸Ó­‚—F±¤>šmê™V½'†=ÝïŒ<;é”i½R\ ÜD=UØÓ-µ'†=Û*ø¼¸‰úÈy⃒Î^JOzbØSÝŸ£á€œ§[íži[;c!Õ¶ót«á“£ž÷LǪ§Û.Ÿj)¥‹zbÈÓý,Óª¥øý‘Ûzt&æ‰KêÙ¶¥t@©$oÓÎõtKí©V½»YROó”Ïgõ{º¥õLŸ7r›Ä §ZåžxY)Γ/PLéVÁœ'/mg {º§Û¦žójx·QOÜ).ÕÒúÈwª¸'¿ûHŽyºUîé–Љ:`¬¤[jδ_Yº¸g:F=9쩦—Õhw”1ü‘¸g ûÈíÒE])ΕæßK¶ÈfŠzrÜÓý{ÔKè#F³ú=q繑HKg‡=y§¸ÄérYB'ä€RK·*>Ó{ªÀgº’î7'™¢žjU{ªUï©öŒO ÷È@Wß'ï!ÏR: \庴ž)ð©þ-ænvœ“”û’zrœÓí‘—ôÄ`;r·tÎR: ¹ÙqÎÍ)Ýj÷T÷éÚh·©§ »”~I<ÕÇÎ*ÅyòeÊÛ°'ÿ;Õ)ÝïäÅM4SÝ&UxS;ñòhcN॒nÍtòe7qW–Ën§yŽ|£žüótÎúTçn§ÀXJÓla9Ïp·AÏôsIîC™KØ/çq‚(W¹„=ñr®Ï+èRn±t\·áv»g;A” ·Áun·{¶»ÚΞk03Ý>S¤3-çrŸ”Z¦ÀfŠt¦¥ñ\î3­|¢™k„Y"˜.×%x7¿›íºsäÒBí`7šû ”Üw4«Ós>´m4ñ,ÄöøÑÜ'åÀm|s‰t^Ǫ*¢cõæ€r–÷ÇŒòw%'¨DwFòDc`"À°‚F<™ý&Œ_ÿ·±drÿ¡IEND®B`‚OTPClient-3.2.1/data/screenshots/hambmenu.png000066400000000000000000000745521452112020400210750ustar00rootroot00000000000000‰PNG  IHDRõN®­ž:sBIT|dˆtEXtSoftwaregnome-screenshotï¿>'tEXtCreation Timeven 10 feb 2023, 16:32:23í§ IDATxœìÝwtTuþÿñç½3“^IHPŠbÄÎZWqAPYYlˆîêîÚWAá·‹ 芫.ˆ¿é„$HH(¡†ôL¹3÷÷G !$3“:)ïÇ9÷Ìdæ–÷ÜÌÌk>÷~î½ „B!„B!„B!„Bˆª(ž. ‚¦T‹BQº§ Ï©§—/„BÔ7¼'BU‚\!DkѨß[—eÈ!„MM]‚ºAC¾¡BÓÝùÖdùðB!<¥&aìî¸õðõ”î̯ºqê2­BÑPÜ ßêÆ©Ë´5VŸ!él^U=W—Ç„BO¨*€ëò˜;Ϲ­>³&aîìoWãÖd¹B!D]¸ ÙÊÏën>çjÞu ÷†êÄæNxWè„ºBˆ†âîæôêÂ\wñ¼³e¸z®Z ê®ÂººÛêswYB!D}¨i ; sWá^—}ñ—¨m(Ö$Ðݺ3NM–)„BÔ…;!ë*¸u7Ç©É2ÝR›@tÕ™ÍݯêogÓ»³l!„¢.\ups7È«úÛÙôî,Û¥šbm½º0wö•îW·!„¢¡8Ûdî*À+Õël9Ϋ’ÑÝ©Y » qw†ªæåª!„¢>8Ûî*À{UËQ*<¯T1nUU©&¡îLMC\uñXåyU\FåûÎB!ÜQ“ÍîU»ƒKC¼ªÇª ùÊÁ^k³u'ÐÕ*îWõ˜;-wg5 !„õÁU WT.q%9Uù1gËS¨>àÝ ýÚ¶ÔÝ ôÊ®:¹ï*Ü«ZnU5 !„5嬥î*ÔUÜV hG¥yTÝ%wB½º}Ù5 ôÊCU; wª¸ïªN!„Â]®B½ª`¯*Ì˽b8—;¥·*µ v—A_×}ê•7‡W ôªÂÛ@Õ!ßôÚa€w5Ëq§!„¢&ÜiWÇœ²}Àvà‚½bÀW7GÕißzMÃÒY+½ºMí'÷»£ýüüº‡‡‡ûûûw4¾ƒÁ¤(’ÑB!š&]×±Ûí6MÓŠ ž={6·¨¨(øHì\öêîWÕÚ¯Í!oå\%§³Mïîln7TsëŒóõõëëëÛÞn·c³ÙÐu‡Ã®×¹ BÑ EAUUEÁd2a4)..>sôèуÅÅÅk€Ï+yå۪½ºãÛ+ª6 ®ê®â~å`w¶ïÜÀÅn‚€¢¢¢®éرãUº®£iš„¹Bˆf£¬ªiV«£Ñèß¶mÛhUUý º»[…IÜÙ]yº»ÓÎCÝÝVº«zÅÁ˜Ö©S§¡¡¡}‹‹‹»Ýîn­B!D“e·ÛÑ4M nïããã•››Û Ø‚óNqµQmÈ×4Ô]µÒ« s%òŽŠŠÒ¯¸¸¸æ/C!„hâl6þþþƒÁQPPà $¸˜ÄÙ>ôu0s7Ô]íK¯j“{å@ïêëëûh‡K/8!„-–¦iµ+((0iš¶È¡úÃæÜéçVnÖå¶ê6ÃWû²`ÿCÇŽ;›Íæj ³ÙlØívg'Ý MUUŒF#Fc}EX´ýù¬ÍûP×uŠŠŠ(ë§ÓÐ}tEÁh4âç燯¯/rÔŽ¨Èl6+:tˆ=tèЃÀ?¸ô÷ŠyYñ˜v¨å¦ùÚ^ÐÅÙþõªZíüüüâE‰ªêC¦( V«“ÉD›6mðöööȇ###ƒ˜˜˜F_nUu]GÓ4, ‹///—ËÔ4sçΡ( ^^^nMSu: ÈÏÏ',,L~ ‹rº®c4Ûûùùõ,**jçâÓÊV ùŠYêÎE^ª¤VóxuŸ„ªö«» vb³Ù.™!€ÕjÅßߟ¶mÛâãã#¿v=HQ|||ˆˆˆÀßß«Õêé’„‡Y­V¼¼¼Àd25Êç³ì0¡€€¼½½]¾u]çܹsF|}}1 V§Á`À××£ÑHvv¶ü±Ùl„……Qr‚µêΰZÕ@¥û•Uùxu¡îjBgg’«ª'üåF£±}U3·Ùl˜L&‚ƒƒÝ(E4¦àà`ŒF#š¦yºá!6›­<´<ÅÇǃÁàô}XTTTÞB÷”²eK'`Q™Édj\Î¥gU­j×uWÙ[%wB½:ÕµÚ«ú%øU5»ÝN```Ê )((HB½³Ûíx{{»±y{{;}{4ÐËxyyQTTäé2DÓãOIºj©ƒóÖ¹KµÙùS]+ÝÙføEQŒUm–Òu½I|EÕdŸzëVºOÐÓe`4¾ív{“øQU•Æ<÷†ªªxyy¡ª*CÉÁLe­V«Ç;K}%E1¡¸·Ù½rÆÖè ¸&×S¯î—„;ûÕ½©æ—‡®ë¨j]6ˆ†¤ªª„z+¦ëz“èã¢(ŠÓ÷¡Ãáh2u6FP™L&üüü\þ±Z­Q]¦†"õ]B¡$ÝÙŸN…û:ggƒ]O½b¡Îm+/\‚A!êFQÝÞ%Rv€Åb!??¿Qñ“ú.U:ÝE™È¥yYUK½Æj»ù½ªMÎZêB!êÀ`0T«Ý!ÞÞÞ òòòl÷€ÔçW-õ2•os›«Wçêƒ;‡¸Õù—‡pê¯ã5°SœüåháY±±±ôéÓ‡ˆˆNŸ>ÍÞ½{ÉÈÈðpeÍ¢(µ¤2F£‘   rrrê½E,õ¹W†‹ÜËK§A_ÓWX]w{WæDRL:ÞWYðîo£^ûKˆºSÃè}ó®Š!óÛ¬:Ú:/X4xð`bbbغu+Ç ::𫝾šÈÈH¶mÛæá ›—ÀÀÀzé°h4 $//¯ªº@ês[M:ÈU¼ïö·zm_eu¿*<ÒR_»v-C‡mÈE4Y†~·¢U蜣@ð³ç/Ù®à0ƒý„ ËVoì§=ß«¹Å1öaôß_áN¿c|º~!«Žþ™º`£#öóá¤?òùÁú;DPõ ¥ClZƲšÈáѱ±±ÄÆÆòí·ßb±XÊOOOçĉÜsÏ=œ:uJZìn2™LõzX¡··7&“©Þ:§I}ns7Ì+ækÕõ¶Ê…T·)^4ÕÔnVŒqV —bÏj¾Á®Æsë#1ê¦AôŒÁh=ϱä-¬ürŸ®:L¡Ès×ñö°ê?ÐÖ ¯0ì©Ü2k>ººÁ¾*ºÝJQÎ)Ò·°ò ùrý1ÌåËìÆ-cÇóÀ̓¸¬c~qúH"[ùŒ.XÏñ*–aˆº‚k. %À0€k.ó狃¹õ²1E º“Ù¿¾ÅpÓ~fÝ3šiMc«@Ÿ>}زeËE^Æb±°uëVúöíëñPÏÌÌtk¼N:5p%ÎùùUyŠ:ñ÷÷'''§^æÕœëSUµÚ£œ=W‡úœmr¯.[ë¼O½ºvõxU·ê Ì~ÜHÁ§Ax 0ã=ÀŒb*y<÷ýÐKG6ê‚t¼úãÕÛŠÏ3…_4nÁõD ÆËÏà.¾Þ`Þt½êNºÁ‹¦òØŒvVkɪÁ„Ñ  Û5lö’«ÕfGLJ6í£ñU±› (°ð íHï:ÑëÚ¹ò•™ºì„çåß½x™Ñ®ÇÕÜaÛ‚뫬՞öo¾À­á)|½ª~EÅhhz¯¶mÛ–or¯ÊñãÇ>|x#VT5O‡µ;ÊŽ£®o&“Éih¹ËU}k×®u:}u[X£>UU™1c¿þú+Ë—/¿è¹;áÇ3mÚ´*k¨C}UzU·UMWÕWÇ%»ÓLs¶WA.¡Þt›‚e³/Ö½Þx4cНf³¦`ÏV0¯óë·CT3=Sœˆéçþ.¾èù‰|ùÖ›|²6ÂàÜ2ée¦ÝÕ½ÊÔ÷ðêÔAü€Ãÿ±çOñ/ÏqÍó«)?›¸!¾ôޤþ1 ÓQBûòØûñÔUa\?æ:/_J×K—YÂÒÿà³õ©œÑhß½?=ØJ†ª/flêǃÏ>Æ~ÇÐ7­âÝ="üȳLy`(=# äÝÍÊßgö² uÀÐQo¼ÀïûÄL Nþ±Ö|:ƒ™K“)¨ø16öâ™öñ `Ûñ6·=ú_²SSæL/R1¯óüÎÅx¥-WÅ»yö¨SÂoâþáa¨z1Ûç<ÇÛß÷ƒ%›NX%ü±G;n»÷ÞÛ¸’‚Z,C;ŸÀòµ‡xòª~¨á„‡à÷ÃÚ êf¶Ï™ÊkKK—I.y›“R£¹û3ðÙùüëᮘ¬9?e¦MìƼÞ0Ë}<ÿóYtC{®6ˆËt,Ùä;ˆû_™ƒãø¼¶©Â©Hu3çg‘c-+¦±^Ô‡²³œ5„ú8éWs¯ï§Ÿ~àùçŸJ®uðüóÏ3{öìòo új›© ~ò™Ê ¬|¿Þ[êÕýº«êñÖÐy.hrè÷¯O—Òh 1]‰5)`Ocãæ,.ú-­fÓ–<Ù£¾±q´7@jMSÎàC›ÎC}{wŒ€ýä Îv*[f:›·TZf ©‘#ybTLÖ½|ðÀ£,8¤uÿÿcÙß®eø7±ò N•ì8Îÿ&äÝ„HFÿû;þ<8’aÃ{óö¦­”og±â?O6}ê“&MºèÖÕ¸óæÍkè’ªÕö©7äÙ6ë#[B}?ýôŠ¢ðÜsÏðÞ{ï]²9¾ë«®#\½ådmisÖ[¯A ®*¨=ÙûÝÕ&¤2 UŸâSË37ã )ŠRú†Òëùè=#½Ÿý‘}Ï^xD·Ÿbբ嵯·e{öårEéË3ËvñL…çìí¢i§r!ÔËŸ8ÉÞ„“8ÇÜ6#ÐTwž¸ÔîCjûÔE‹âªá[ÝamrH›»šqr¸§¹n hÎ-{GFšN;¯. ÅÇiÇ/´œq\=¨=*:…™édÕ¨ñª—t¬³X(ÎÉâpâV~ñ¾Ú~{ÔQN8t:ctU$ó¨}k]UQÝœÂÊÿ®+Ù_VAÎ.N:¨òìZÙ5ÅUCÉK/ûQ£¢¶øOZëÔ瑯3·µ„ú*îC/Ûü^q{Ô× ŸÖÆ8žI¾nnUP¼t/½|_yU'æbK5aÙîƒ^Ô¼Ïä8³šo7Láª! œ2ƒrÞà“ ˜ƒã¹ù‰¿òX#Šý«–m¬áþt;ûgߢ…é—î—>½ŽÕ{žaÀ@_þé}^*z—ÿn8È9«¡.çÊÈ“üük*VÝŽÝ¡ƒLT”? —öv×¥pHɦ6xe­àã%©8À+´!Öœvó{R·äS`ÕÁ¯ÝâüQç¡ iuÚ=PßÊZäžÜÔÞ\¹ ŽÚö.‡ú 䆼"]cÔwûí·W¹½l»³`¯e} ž‡õqéUWãˆdÏ2bŒ±á3´Ëô|ÝvéêWx÷·àÕÇ‚e‡Ö>UŽ×,ègùùí×¹¦ûÛü®c_ÆÎøŠ±=oæðׯ1k]~ýmžwcé?æ0|áó\Ò›Ño~ÆèŠOŸý–œ-/³Î’Áá#zß n~û[þfÉß7UšÕÑe,øá|pO4Ã_þš OçP û`áûÉÃxyƒ·ØÙº»Û¯ å¶¿0à¬ߢxâÞ™ìmªÛæ›æ°OÝjuþ^¨Ë–BWó®¯y¸»‹²6ó®ËöáÇóÃ?TÞõPŸ;ùY/Ç©»«™&BËbÞäC@´†W/+^½\¿Éø\mÆër+E?ûc?Þc?¹‚¿Œ:Ê–ÇÆóûáéÞ!“õõ²ëz×÷¡m6›­^Z®ê«íŽÆ¨ÏápT{úòåËùù矫­¡¾ês¢ÖùZÝl*Þ¯8T¼ºŒ¡Â`,L¥ƒW…[/`N\\\ߪ (**"&&¦¶õ׫ŒŒŒfY‹!BÃû*3†öª/`p Ž<•¢Ÿü±Ÿpê rÖ(ÑôѦM·Ç¯Iöšö~ÏÎήö}˜••E@@Ó8‘RAAíÚµ«·ùFBC«8™Tœ?M«Ÿ_~RŸ{ÒÒÒöSké`«pk£ä§¸Ø+ Ž ƒ^i(Sã“ϸKZîb?m¤èGç_hÁÏœ¥äD5Ö]>X¶y7ßÍïB´"š¦a±Xêíüå‹¥Þ¤¾zâ±CÚÜ!IÑ9ò رîðÅQ(ÿ"Ñp<}¨ZK”ŸŸÁ`¨ó•Æ4M#??¿žªº@ꫵzÿ2nȪ’MHþ O— ZéáÞ0t]'//¯N××4¼¼¼z¿V9H}µÐ`ùؼmBˆVÂn·“““SåÕï\±X,äää4è!hR_ÓÐ<»> !D+TÖâ4¸¼0ŽÍf£   !öK}M”ÇC]QGƒžCXÔžÃá@QdOJk¥( º®{ü=àªUU›Lñ]¦i999å—UUµü\äv»½üP®>ìJêk‚šD¨[­V|||<]Ѝ‚Õj•\­˜¢(hšæñK¥jšæô}h0p8 zÕ0w8Ž:wƪéòêzyÒ†$õ5>[ òòò<]†¨F^^žÇ¿(…ç †Z탬of³ÙéûÐ××·^Î@VWV«___O—!Z1‡ºÉdBÓ4rss=]Ѝ$77MÓµå!š“É„Ýn÷hkÆl6»lûùù¡ëºGƒ½lÙꓚķµ——………X,‚‚‚Ê÷qˆÆW¶/)//MÓðòòjCLDóáååU~ oooŒFcƒï»ÖuMÓÊÝÕiRE!,,ŒììlŠ‹‹Ë¿C£Î²Ï @XX˜Ç÷ë‹Ö­I„º®ëå-ö³gÏâp8<$YnU€‹þ;º™£3±è „v¡sXÅgUBâb Ut´¼4RO¸ 1ÝáÀáp 7ÒgÝÉ‚2S3(¶;Ѓãˆmsñë Ž‹%G^׺ ;ѽkèeÊ‚½°°íg|@E×q8ôF[_B¸£I´Ô®ü˜ƒˆþ0·Ä›°§­â“ÕéØ_ºß:†‡ŸFQÒr¾X ; a~Ïï´Á~t=Ÿÿ|šÎC¯¦gd(Aþ¾˜Eç³HÛ·Ïa-ûÐ)þD_1ˆ=chë¯bÍ=Åá½›Ùšz[]?˜¦Žôê†â8ËÑ“þDwè@¯ža¤l?{¡uªxÑs {ÅâƒbÉçTâZ~Þ™…æì9Õö½1 W þFlù'Iß·­É§J¾Ð½»sÛ¸atRN—¶ ýµ£ÙËKòO|¶î(%ŒË†]MÏÈ||ð6%ïGömfsòÙ ë@`Ѓ3pœØÈ?$PPo_\ 1¸f`:´ñEµ™)<˜M+7‘QLµÏe:â]¿Fpý?Vƒé2è*.ޤM°ÞŠFQö~V»•¬šþvT|è|ÍPz¶1‚õ4‰¿mdOÆy4Ÿp⮼–!ñmï=”«Ž.a½³÷xùzoSzGçìÎoønwøFrÅM·3°½/zw#äÐnÎW¨Órô æ.t÷ ¡K—¶ì<{»®ƒJlLÍFîÁœ´h7àf®îŠ¿ŸªVLÞé#ìÛ¶ƒÙ(Ž’CºÐ°—†»âÏÍ£¯§£r†íßüÀÞh7än»ÌË•|¹áXù:ïÐgWvïD¸¿Š5ï4é [Ù~0ÛéçJ×uÕü`7g ½ Ý|ý‰‰mÃŽ3gJ—Jl§ ÒוâÖëJÛ¼GQq­½LÅ{°’W²¾ ¡\yß#\ 8²6³äç4ÚÅu ¥¬`ÉoDZ£ÐæÊ{¸çÊPìÇ6òŸ)µ®¡)Q*5*ÿ-<£I„ºSº™c™'qtŠÆ/ªm”cœÁŸvíC@wp:ó(f%Œ¨Ø´5f-¦X÷Â?,–>ÃÚÑÖg?ìËÆ‰vWä¶Þ¡¨3ù6|C:ÐkèH|íKXu¨¸E*øÆ]F?ÐŽígîPF¶ëCH÷Ëé°{%[DÉï®l‹šÕ‚f À[7ãpöœâEûÁ#¹£w 8°YíxGsÙu‘„ù,ãû]gÝÛ¤­”¯#»ÕŒÙjÂ7¸=¯½=o1ëUØQ kç`q€£ÀB}6D¿î\7âJbŒ:æülr5/ Ø-:Š_jŸÃäÎÜÿW*FWBˆéG{8lfŠíF¼u µ¹2âÛ™Ëb}PtÛV³15¯d]YN´n ~a÷Ñ?ÜŸ®=;²åØa¬µX†£ø4‡2Î3 }øúã \tJÛ1RåѵWþ1 ßvгªÀ¨vp,í‡ ). ¦k|ädß®J¯!<Žøð’ûY[6Ü•øÒ“?¦o\O:@DWâ#€³»øm=€]ãã/̨8‹{²8Pþ@8»…×â_µ½´Bé¡£Z~ÂÖW|ÜÉë*,,ÄQTXç@/S1ØVþDzéYèÊ´ïÚŽlbú”­£‚D6”M|ÅõÖL麎ÍfCÓ4 (((Àn/ùy*áîYM?ÔÑ)ÊÌà”£íC¢‰ÙÎÙ¢vth«‚~šÌÌ"tÂJGÍcÿÏ‹Ù|ÊDÔU#ùÝm‰êׇ褵õéFÿËBP§Ø¶ô{vŸ× ¸ìV¸®#±—uC‰µ/Q $¾gGLXI?NþɳÊéÍ•¡q\Öyǃ<ú„c¤˜Œß~`UR6&¼ ºO'Ï]Æ•—…bÐ 8ôëwüz¨˜À^·rï5‰è{1‰«K¾tÝ^y$þ¼˜Í§üéuÇ(®‰ö'¦sê±ãÂ[ÏfßO ´ŸÐ?˜ £‚n?ƶï"©@GQU‡‹çÜ 8ÿûN¤ ü5æ‘ðÓb6ŸÔQU…ÚìáQBÚ¢* Ÿç豂‹ü8ÎsôDW†a %@…ì.CWøGsy—p@/* ð’±ìœ¥M<Ý# `>ÌÁL3ºnåÐás\10ŒŽ=âð?´Ÿâð("Œ ºù‰)Ùh:€ «†Nž ‹ ­AA7g’’V€ÈM=HÖÕÑÄšÚªžS‹¢õN)€è|ü|QÆøÚ×Ïg’™Ó‡Ðh®{p ñi)$ìÙGZ¶ÕésîPÜÿ.„ú…ŠÊ÷q×üÅ”½ñ]MëK IDAT\L_ãï6•ˆAòD… ë¤í;H^¥º®ã8“LÒ¡(”3‡q¨*ÉÉp8šO§8Ñü”õ1›Íœ?ƒÁ@»víˆåäÉ“–üü”`o|Í"ÔÑ I?˜Å vÑ„Çu¥[v¼qp&=½äK®ªNÅvPE¥ä»U×Îq8!ƒ¼ ßú9«ÅˆŠ'D)íý>þâMkJ»t IfŸRöÝ^EÇgϹAG/‰Å€Á ãnŠ8*´ÞÊ—qAõOËbë÷ß“Ý÷ út!2~‘q1ì\ö;Î8y.Ï×èâ\àþªq‹žwž]'@mCt´?{rò/Ä»Btûtl¹9Ô¨•®£ë%èlæΟ>FZâ>’N^ôóA×ut]'$Ä@ñ±äfŸåøñcõöú„p—ÝnçØ±cœ?ž˜˜²³³ÉÉ)imH°7®æêè¥%“1(š.}ÔÆŽ,έ´?Ù€A)é}Ð9†¶*è¶Îçë8lgÉÖ»¥ú`,8Äž¤l¬:|ñÑ.m¿¹ÍØžn]Qu–Â<ŠËH*ÞøÃéÖ-Œ}Îq^׉ôŽ¡g· ²äaGÅËKÅv¾úç´s§9c'Ú»=â8q¨˜Àøn´StëYÎäè`+¦ÈªƒO‘‘þ¨çjwíz]³a³ë` ,Ô„rÞª GýíWW½ñÖÏp`ËJì£ÿïîc`d±‚Øu®¨úçv»~ŽlçÿãÂz¥q s :ûÐ~àM\mÞÀ¾Ìl>at¹òzú…©(ŽÒÔôÐF3Û¾rz`Y ÂáÔT,–Úýß…¨/……%ýºté‚Íf“»4“PÝ’ÉþƒùÄõÂˤcËú\ô¼Æù” lɬßΆeŒF#‘‘‘¤§§K ‹&Ãb±™™ILL GŽ©öÑ0šÄqêîÑ8•˜Ä‡º™#ÉiUþ¦Ôíæ™Q}¼À’˱Ä_ùyó‰’V’^LÆúïùiÛNäšÑÞøìž;C¾½ú“‚¸Ò©[ >ŠŽv<Œ‹ÎŽãàÌátÎë t¡[”ôu?°rç!NåYÁä—^LŽYÇD±“ç¬Ûô#?m;ÄéªIÁš‚›—³|çéÒ“‘X9ºy%¿%g‘go?_ öbòÎ'ót ¶Bè…Xÿ+{3³1;LøùªX­v õø.QU39çͨ!„‡ùCÁim[Åoif'Ïéî¼Fÿã†h+8 óß~Çÿí>LVN16»»µˆóY‡Øõëw|·!ƒâúÞBPÚJ'''‡¢" tÑ´““CxxxùûU4Žª¾ç”J÷+j…ÁPa0RrБð*½_ñvNTTTßàààÚ©(è:øÆ åÞ[â ÈÛÇw_mæTYÃËÃð‡o%Þ˜ÇÞï³ùdýþ24™LØlÍéÜp§¥¯›¦øút]GUUbccINN––h’ =zô(o­·æMð¹¹¹œËeƒ£Â WÊ\ô‹©l~7wã¸6JÁèïƒ 3‡wìå´¡#Z¡²VO`` ùùùè¢É²Ûí@nn. ûÖCÓßü®úácr`ðõF1g“¶yë5È>J!šƒ²rùùùž.E§òóó Íï¨é·Ô¹ìÿù¿ìw6Ž=ƒ_?þˆ_«&!<Ìd2Q\\—³ 6JÑW  SÁ6,ê-PQQ‘‘‘ž.£Uiú-u!D¹²ÍïƒÁùiO•0F}œHÂ?‡ã•W3Æ^L˜3—©×µi°Ó"ˆºY¶l}úôq=b54MÃ`0Hg¹F$¡.D3¤ªj½œË\gf͚ży󈋋«Õôš¦¡ª3IÖ¶­Å-/ÿ‡e«7³+!‰¤„í¬\81>ËW±uo»7.cÎÄþ—5›ÕHnzi!ßü²‰]‰I$íÝŠE¯3¶_èÅ_††LšÅÒµ»HLÜÉÚ¥ïóøà¶Nô¨†sÃÔ¹,^±‘]‰Iìßµ–ÙwG”ÎÃH¿W“’šJjjsn÷m¼u"\Z»v-³fÍbþüù„……yºá†Fݧn2¹u©­&©9×ÞÐZúºi*¯¯lf­.΢ÓmàÂRfðôŸ“°÷á¡??Ç+Ó’XòÁLž›UDذ§øë3ïòÔî[ys‡ ”ºDä¡ÙL{5‹oG?8™é÷$pôhþ™l|è;u>ó‚gMcæA…ø»ÿÄÓÍÇç¡Q|˜`%ŒÞÇÑ1ãC^øûNrÁØÓÏá ÐH^øG¦›…ù'Ìõ½ÚD}õÕWDEE1þ|ÆŽ[~–¸š0 E‘ð ¬QC½©ï+Dsã(½ÖyíèÙŦm hìâdÌ- xˆÕÿ[Éo6`7 ¼ó_ôïßÃŽ´Ò üèä§maÝÆ46óÛ†dô¯óðøëY4m5…AÙ0¦ þu'¯|V2Í–Gð‹ÿŽ å“§WRÒGßAÞÁM¬Ý’@ùNƒÒßJæ3餦fºw aásçÎ%**Š÷Þ{I“&ÕxzMÓp8²)¾È¢U²sæät¿6„ú”=t–SgÀ?(°úŽk–d6l=CÀe½èdC\/ºûd±c{æ…«üiGضã¾=úÛô¯¢E‘œ­”ͦ¡+& ªè [±Ùu—­)]ס|ªR·žëÒ!ºY˜2e ={ödìØ±ž.E¸ -u!„û èׯ-æC)ÕÀž–HйvºÐ1ÎËÀþ‘˜$pÄY}݂٠ÁòEÔ„=ðÀÜu×]Lœ8±VûÓEã’–º QÃ'0éÈìÉ‚¸‘O1©ÇQ¾~w}ɾò¼_Yøùa=9‹×ŠgóýA…îwO剮‡ùôÕµ8=ç=ý,Œ9‰1{¿ UiO›ì5ü´;WðMÄСCyúé§3f çÎót9 êB't¬¶†<ñ“:x‘Ÿ¶•/ŸyƒÙ[ËZlÅì™ý8O¿Ä³grWœMYÇG“ÞbÁ>=ÙõVÌ|•ï<ËÔ9#0d²aö>VìÎE.íÐ4<óÌ3Lš4‰ôôtO—"ÜÔ,®Ò&„(QÖû½GìÛ·¯afèÎSß,eä†ÑŒœY¡×º5ЧORRRPUµÕö~oÌ«´µÎ5,„B´@êB!D !ûÔ…U³`î]½™ëé:„n“–ºBÑBH¨ !„-„„ºBÑBH¨ !„-„„ºBÑBH¨ !„-„„ºBÑBH¨ !„-„„ºBÑBH¨ !„-„„ºBÑBH¨ !Ÿ@t¿¡ éPåõŸ…µ#¡.DK¤„1êãDRSS+ I,‡ÁÓõ{1aÎ\¦^×FB½ [¶l}úôñt¢ä*mB´`¶ísxüýM˜ËqPtìvÖ$šY³f1oÞ<zè!ÒÒÒ<]Žpƒ´Ô…hÁ¹Gسs';ˇÝ$Ÿ2 mnšÁº„åLïï_2²± ~¾“ß>¸¶ÆHnzi!ßü²‰]‰I$íÝŠE¯3¶_èÅ_††LšÅÒµ»HLÜÉÚ¥ïóøà¶¶¨áÜ0u.‹WldWbûw­eöÝ¥ó0ÒïÅÕ¤¤¦’ššÀœÛ}oÅ·¬]»–Y³f1þ|ÂÂÂ<]Žpƒ´Ô…h•t²W¿Í›+¾cæ«“ùåYäÞÿ7žŠÙÀ+O¯àŒÞîƒyh6Ó^MÄâÛ‘ÁNfúÇ= =š&[úNϼ‡àÇYÓ˜yP!þî?ñôGóñyh&X@ £÷ðatÌøþ¾“\c0öôs8ˆ4’þ‘éßfáÀAþ ³«¢…|õÕWDEE1þ|ÆŽKaa¡§KNH¨ Ñ‚yxÝ©ï•ÿí8·˜ ׿Âo6@ÏfÕÛoòËwïòÚk!ä cÃ_ŸcÅ%Mmü´-¬Û˜€Æf~ÛŒþõb=‹¦­¦0h8ÆtáÀ¿îä•ÏÒ°[vÁ/þ;&<6”Ož^I~ÉRÉ;¸‰µ[ÐÊ 1•ܘϤ“šš‰£ñV‰¨…¹sçÅ{ï½Ç¤I“<]ŽpBB]ˆ̺å=ƾ½þÂ>u-‡£Ú…çõì_øÇŒ‘,Ÿñ{¢{…[Wœ©>`-ÉlØz†ñƒ{ÑɸšÔ¸^t÷ÉbÍöÌ ûèµ#lÛqŠ)×÷!Ö¸’½¡^™¢*êB´`zÁ R““©vƒ©Äe{âSX}oaxô×|™Y}7:]×A)믮ԭçº~³1eÊzöìÉØ±c=]ŠpA:Ê Ñj)„ÜðyŽÿ7v ï§\Á³¯>HLuÇ»:ѯ_[̇R8ª=-‘s; ìt¡cœ1–ý#1HàˆVÍ|t f3È—P÷Àp×]w1qâDÙŸÞ HK]ˆL í€Aƒ¨ØÍQx”ý‰'(ÂÓ/ßAîÇà?I)8þ6—[–>ÍË¿_ËK D ŸÀ¤#?²' âF>ŤGùúÝõ%ûÊó~eáç‡Yôä,^+žÍ÷ºß=•'ºæÓW×–îO¯†=ý,Œ9‰1{¿ UiO›ì5ü´;WðMÈСCyúé§3f çÎót9 êB´`¦þ“™ÿÙä‹Ó’?äž{ç¡>ò÷ò5’Œ ísþñù}|ñÔŸ¶ò@Çj aÈo1©ƒùi[ùò™7˜½µ¬µVÌžÙódñK<;q&w…ÁÙ”u|4é-ìsÑ“]ÏaÅÌWøÎ³L3cA&fïcÅî\9†¾ yæ™g˜4iéééž.E¸©ª]bJ¥ûµÂ`¨0)éÏj¼JïW¼Õ788¸a^…­„ÃáÀápУGöíÛ×p 2tç©o–2rÃhFάÐk]ˆêÓ§)))¨ªŠª¶Î-¹¹¹œu!Ä¥ì˜{Woæzº!DHK]!„h!$Ô…BˆBB]!„h!$Ô…BˆBB]!„h!$Ô…BˆBB]!„h!$Ô…BˆBB]!„h!$Ô…BˆBB]ÑxÔ{Ž 7„7ì—Ê•£žaòÍÑr9B41êBˆÆ£F3ü‘G¸5>¨Êë>_B ºßP†t poüòåD0dôxîìZ³é„hæ$Ô…h‰”0F}œÀ®¯¦Ò/°R¬y gÆÎD=Õô¿Œ½˜0g.S¯k#á,„šügZQ[ }&1ç÷ÐQ®Ç(D« ¡.D fMÞÊ‘þ/óÁ“}ñs6¢±=C§|È’U[Ù³o¿~ù6£{¢à͵¯odÿSéYöÃÀë:ÞØœÀ¢1‘¥_ ¡÷ý›½»æp{`¥ùzwæ¶çóÓ–½ìß·•ŸLePpÅ6·ÎÎeÅæ]$&%±{ÓÌ{ú"/Ún¤ß‹«IIM%559·ûº9J›ëžgÑ/ÛIØ¿‡M?~Ä´›:áåzŽzƒ/×ì 1i?»6.ç½{:\øÒ¬v á9êB´`Zú—L{q9ßç•ê ?ú?·€Ù÷û²aædÆŒ›Îÿr‡ð—Nçú {·ìÂÓ‡>¥alìÒ¾Á&z^Ñ o¼èÕ¯7JÂfvV˜­ȵ/ÍgæýmÙõÏç™øÄ‹|´5SÅ*œÝ½˜™Ï=ƃ<Ä´OŽÒ}â»LQ15’NäΑ#9òÞZovs:o¯6Íÿ “Ÿ˜ÎGû£øÃÿfÚ ·–mˆ„¼2Œü%Óxè¾ûyì¥ñ}â.×YÿiBÔl”¢EÓ9·ö-žý÷g,zýu¶%OáÛ³¡`ü¨H6¾:†¹+Ï£ɯvàúUäÖ¯±~û&öñ,ûù±xu1m  CQ>ô»ŠžÆ5ì"žA8ôíÎ:*Ì7äFº+’}³坋ޖ„áö\zÞ{WV¨/ÿÀV—þ•˜ü>Ýnÿ–{¯è‚aå´ÒÇÍgÒIMͤÂìݘÎNÖšÌû* ذù Þ]~à‰1×óáÖŸÉw±lBÛªä²qÛVö$Ar’ÛëlÝZkþgBÔž„º-^1‰ó^àƒ‹ùóÛ£Ù;ùøEÏ:÷$Þןom áͲGU &æ¶pö7Ö&ý™G‡ôÆkM2Ç“°ðß=vƒã ìµâªö¬[Ÿ‰½â|c{ÐÅ+‹U»³. ã‹yÑqÄd¦M¼•+:·Åך‹ÕÏ€¶ËÛÅkªÅtöLvï9ƒÏžt2þÌ~Íù<´]‹Y°éf¦ò#½~ü_|¾”ÕÉÙhn¬3…lt¯@ˆ† ¡.Dk ¥ñùŸßᚯ_âõ‡æqºâsŠú~|é1æ%jžÐ):“‹îÈeÃÿä¹ûnàòÀ®ë—Á†÷~¤pГüîê,³_Oü‰u¼qP»x™º((NvòâÇ3ûý±Ø—¼Éô×9§Ç2êYŒ¸h>µœ® Š¢€î@wgÖ,züÖ ¹›‡}„·¿™ÀÃŒçÑÑ\­3uÑP$Ô…h%ìÇ¿áooãÛ¿?IOoH,{üH ‡,ãècàè÷©Øª˜öȪ•|ê^F>Îà³›ø8óëR™vÓýŒrôæäš™$UÊt{Z")æq\}MWL»Sªœ¯w|oº©;ùÛœoÙœ«ƒZ@Fn…v½nÁl†ÀàT(oñ»œ®*^=¸æª¶&'’©¹9½ŒŸóæÆïøùÅ¥|6ö~ú}œÈ7Ö™ž ¡.D«áàäo0ãÖïxëÆ ›©õó«ùté$L˜Åûöñõ®,,¾íèœÉ7ËvS ƒ=c%Ë“¦ðìãQ¤/ø5Ž_áà3Ïñ˜šÎü·“¨”éèykXðßC,z|0—ÅÛO`õëG¬ß…®lÖÃ8¢ŒcÔïâÔòT²µ(¢*t¤³§³ÿ€…q#'1fï¤*íi“½†_\M€B`ÜU\;؇bŸh?8…ñÑIükúz £‹y: ãÁA*©)'(2µgP—`È9OŽîÞ:Â$Ô…hMY,{óF^õ— þ¶¼ó(“ÏOã©û^æÃ§ü¡à$VÌà—ï) ({&?~³)½ÃX¹â@I€ý…ŸŸår÷|—Z9ÒÌìýp<Î?ÏÔÑfîä –\NfngUZ@O^È ¯EðÊÄ¿0ol[¹gÓØ›™W2 =‡3_eà;Ï2uÎŒ™l˜½_¸š.—ƒÛvröÆÇù`a^Z.™{W3cü{ü7©¤›æbÙÆ°îÜüØ#¼Ð)/-cûåí—’¢¹¹Î„ð€ªŽpQ*ݯ8¨C…Á˜Jo½JïW¼Õ788¸a^…­„ÃáÀápУGöíÛçér„p©OŸ>¤¤¤ ª*ªÚ:¢ÎÍÍåäÉ“{)€°UºÕJïk€½Âà¨0蕆2ý„lkX!„h$Ô…BˆBB]!„h!$Ô…BˆBB]!„h!$Ô…BˆBB]!„h!$Ô…BˆBB]!„h!$Ô…BˆBB]!„h!$Ô…BˆBB]!„h!$Ô…h­”PúžÆ”[:bðt-µ¡F0xÜsL¸!¼a¿È”P®õ “oŽnžëI´*êB´VjWǽCª¼s“§F3ü‘G¸5>Ƚú•¢û eH·€š½^5‚!£ÇsgŸÐ湞D«"¡.D ¦øwá–)3ù|Å&víO"q×~\4ƒÉ×G¶¾¿±æÌeêum$œE‹eôtBˆ†¡^ųŸÎcBLkÏãÕÝÇ)ô§ëWâkC÷tBˆz×ê~¬ Ñ:øÒÿOoðXìAæŒ{€§Þ]IJUkXýãbæ½ñ4ÿX™]ê*Q¿›ÁŠ­{ÙŸ°“µKfðð¥-YœËŠÍ»HLJb÷¦˜÷ô D–íXV£¸ååÿ°lÕ&v&$‘tÉô€ÆÀGßáËÕÛIHJbïöµüðŇŒ»¬´=alÏÐ)²dÕVöìÛÁ¯_¾Íè^U·¤½;sÛ‹óùiË^öïÛÊŠO¦2(¸â˜.ê-Y ý^\MJj*©© ̹Ý×ÍéTÚ\÷<‹~ÙNÂþ=lúñ#¦ÝÔ /w—­ÐsÔ|¹f‰IûÙµq9ïÝÓáÂpMÖƒNH¨ Ñùæþ;£É^>—Oö:Õœ¶’§Oâ±)o±Âv=Ó?œÎuÎî^ÌÌçãÁbÚ'Gé>ñ]¦( %˜nvp!Ï?>ž O¿Ë*Ç ¦÷¡÷S Yðt_N/ù;“Ï”·ÖÃ×Ò·½ øÑÿ¹̾ߗ 3'3fÜtþ—;„¿üs:×T*R äÚ—æ3óþ¶ìúçóL|âE>Úš©b침ä…¹säHF޼‡·Ö›ÝœNÁÛ+‡MóÿÂä'¦óÑþ(þðÁ¿™6Èß­eâᯠ#É4ºï~{é_|ŸxÔl=á‚l~¢2Dv¡s ƒÔÄ$ÌNÇt³5?­M@vœîÄu_ÝÅõ—Y¿M#ÿÀV—Ž™˜ü>Ýnÿ–{¯è‚aå4t Ò¶±as[Øv²×,¹³dúäxl\7Ž,ø=Ïÿ; ´éÄÃŽ‘(Á#?*’¯ŽaîÊóè@ò«¸~Õ¹uÀk¬[k-¯R ¹‘‡îŠdßìGù뢣%a¸=—ž÷ÞÀ•åcénÔ æ3餦f–j ×ÓÙÉZ³€y_•¬§ ›âÝåžs=ný™|Ë&´ ¡J.·meOr$']xm5XB¸"¡.DK¤((€®×lϹýxÇ!´ V/:Ž˜Ì´‰·rEç¶øZs±úÐvy;þXéôƸ>ôô=ÉÚ±U1®¡sOâ}ýéðÖÞ,{TÅ`r`n€Bvù~Clºxe±jwÖEa|±š×[ëéì™ìÞsŸ!=édü™ýšóyh»³`ÓÍLÿäGzýø?¾ø|)«“³Ñj¸„pEB]ˆÈ~êG‹U®ì[°¸9î°cGÅ ‚!~<³ß‹}É›L=‘sz,£Þ™ÅgÓÛµòéQÐÐìÕŒ¬( ŸáÇ—c^¢Vq.ɽ8ÈtPPœì0t«Þ*Ò±6¯³¤|tº;ó°`Ñã·°nÈÝ<ôè#¼ýÍþ`<~”ˆV“õ „ êB´DÅ[øqU6·Þù8üwŸ¥Õ|®w|oº©;ùÛœoÙœ«ƒZ@FnõíäÊ왇9bM¿+Ú¡î=zI Û~$…C–qt1pôûÔ*[óåã¦%’bÇÕ×tÅ´;¥Êq]Ö«[0›!08Êë©ÕëôêÁ5Wµ¥09‘LÍÍyè…dlüœ77~ÇÏ/.å³±÷ÓïãD¶Ô`=ኄº-‘žÏÚYo°üª÷xé‹ÿÒóÓ%¬K>I‘LtÏ+éxô?Ì\î|ÖÃ8¢ŒcÔïâÔòT²µ(¢Üï­gÿ—??Åœ§fòçâ²â°NÇa·ÓÃ[ýüj>]:‰fñ¾ý_|½+ ‹o;ºgòͲÝTh¢êykXðßC,z|0—ÅÛO`õëG¬ß…z\ÖkOgÿ ãFNbÌÞ/HUÚÓ&{ ¿¸õ:ã®âÚÁ>ûD3øÁ)ŒNâ_Ó×S]ÌÃÐiRIM9A‘©=ƒºCÎyrôš­!\‘P¢…rœZÁ‹÷Ÿf÷“sÿÓ¹#Òƒ%Si{Y»ÈÛå¡/ZòB^x-‚W&þ…yc0ØŠÈ=›ÆÞÌ<÷ Ðsøõõ'økñt™2‹ƒ,Ùw›®ãÐ ØòΣL>?§î{™Ÿò‡‚“X1ƒ_¾§R˜™Ùûáx&œž©£ÿÌÜÉA,¹œÌÜΪ´<€îª^=‡3_eà;Ï2uÎŒ™l˜½_¸š.—ƒÛvröÆÇù`a^Z.™{W3cü{ü7ÉêÖº2†uçæÇá…N!xiyÛÿ+o¿´­¦ëAçªúÙ­Tº_qP+ † ƒ0•Þz•Þ¯x;'**ªopppü !Z ‡ÃÃá GìÛ·ÏÓåÔ˜¡Ë$–~w[ÆÞÆ;{4׈f¯OŸ>¤¤¤ ª*ªÚ:¢ÎÍÍåäÉ“{)€°UºÕJïk€½Âà¨0蕆2ýì“–º¢évËú)GÈ8•‹=¸#&dɪ­ìÙ·ƒ_¿|›Ñ½/Ý FrÓK ùæ—MìJL"iïV,z±ýB+|‰¹¨W  ç¨7ørÍ“ö³kãrÞ»§CÉôÕ>ç͵¯odÿSéYÖòºŽ76'°hLdé²Bïû7{wÍáö@ÀÁI³Xºv‰‰;Y»ô}ÜöB¿5œ¦ÎeñŠìJLbÿ®µÌ¾;¢d^Þ¹íÅùü´e/û÷meÅ'S,;G„û¤¥.DKä;˜ûïŒ&{ùë|²¿Ð鍿´•|øéNÓž¡_`ú‡ÓI¿ýeÖ88»{13Ÿ[ÈÉ<…¨«'ð—§ßezòž[™®ÓmàÂÎâù¿îÇâð‰ÏU˜Þ‡ÞO-dÁc>¬ýgòÞs(íogÚ›#éÛ^…$?ú?·€ÙwgÁ›“yõT×Lú+ùçtŽßþ2ë *©„Ð}ð "ÍfÚ«ÿ¿½;‹ª^ü8þ>3#n .¹›™ â/—HM³2+ÛM+¯×J34×k×%³Ýò¶Ü,SÌ4µ®–•ÝÊ,+\CMÃdUD¼*䊠‚ÎòûБfXTBŸ×óœg³œ9eÞó=ç D“]ñZ:>>œ Ÿ´ÀïÉ'™{(x}-ýyûå;I Oßµi5Såà!œ€Õëmgؾì­iío{Ä…­ImüËQçÆ–”_˜Æi|hÔ #*„­'+ÐæÙ9Ìê K§ŽgJ¢AÀ#ÿàÙÙs¨Ð·7Ó£²Á¨A«»îäÚ½Óyîµ­¤Ûüqì9‚Óð㶉s˜òp&‹§cr¢Úm{2´ÁÙ’ü¿"¦¢¨‹˜µv®÷s’CV÷tr|çJ~‹Âlù£!·ÿ·ÿÏÆÚMv2â×±2÷žÑ±ïÓìÁÅöõkFòÜ¿1îãXÎFõ†<íì€áß•½k³~Rf,;† ˆTŸÎ+Fp»×Yv&ߺºÈH gÍú(ìüÎoëbq}»ˆ§vfÁø•dâ*p}©VjF:ë7md{ì)ˆ9·d£€Û27o ’1´ªÄ¢•§©Ù®õOe@ÐÍ´°­bthçË®Åáñ½‹—ú4!þ£î¼üY |K2•¾'xP>}v¹ß÷‰ Êý>‚Qínúö¨Mä´¼²`N€Íé´xìn*Þ?¿”aÚý.bF†¸\Å;rîØ¿—ýΪT÷·>\ÛõY¦/ eí¦­l^ó1}¯³âãS¾ÀÇÿ/÷ñ¶Æ­iQ1•ðõ‰GšÖë[P±2]Þ\GTt4ÑÑÑD®xž¶å*S³¦oágägDznã!|ÿ¯% mº¾öm‹˜»¡ Oº”“‡p‹êçF5Ýæ:üa1hß©>†í;5ïc6Ví@ÇÆV¬ :ps½½¬Y›[Ò¼ÂA¶lNÁ‘·žöd6mI£b`k0Œ²6 ¤‰ÏA""æ]ä"(ê"&äHKfßi Mð)Æã\N,X-` È´÷Ÿ¢nÔÇLø}žy›Ð¿Ip9ìçņ;v‡—;¸±tâctïÞ=wêÆC<ÌK¡éE:‘Ïårå,‡"¬ï™x ¹GÌ#¾N/Þú.”ÏžiI…Âns`ݯ‰T¿ínðkÏíA{Y·l)k£rÛ-õ©s[g¬á×D;`\üÇ]®œÇëUY.þûˆ˜Ñép–®8J­îCø{ãâdý¼ò­hfÙÊ—!‹ù=:„Øhö¦} éHÙM²£A7ÖõøBãHŽcWv š_ge_RIç¦=¤fx{'àÆÚ  šdíŠcŸ½ˆëë:ÉÞõ ùWð# š„ÖOõ"¨\a·9H^±ŒÄºwÑ­ÿ}t<¼õ)iü¶&À{zÑûÞV¤®ZFŒIÑÄeÕ¥]û†çOŒ³5¢}ÛÚdÅG‘lÇ«œÇÖã–[›RÎûÝD ¤cê"fäÊ lêd~ºù=&~ñ9-æÍšØTNYüiÐâ&®Ý÷¦üTð"ÎìŽ'ÙèGï=Hû)£ö:4ð-ú8Ôut9_þ2’‘SxáôLBw»¸öÎ ´B8à:¶’ùß enðTÞw|Ä·Û’]±.ÍüSønI™ª[©sW0C“—²ý 4î6’¡ûøößkÉl…¬¯µá<ÞÁBBÜN•«G‡&þpüÇ]ßàØ»ŒŸbF1fHöÌ}‚D»çêå$þs,ƒ,{˜óVLαñ«™·p7 †MåõÓÓø!Ñ ù#£y¦énæO Ë=žîåûubs?ßÅ‚!!|À m>À™JA4ª¤³ß¥èu“r¦…ò|¯?ˆ6„^ŸÀCµ+cÍ>AZÒ”/t7=vϽ^‹—¿È¬§|±ž=Eúá$v¤œ(Ú ¸Ž³úgxåôúšÊãU²IŽ<ÀY— § “ðw0üØxFö|‰é#+Cf*ñ¡ï²üÐNì¼LX|'N2d]x³QƒVwÝɵ{§óÜk[I·ùãØs'h3z³úÂÒ©ã™’hðÈ?xvö*ôíͧáÛÈîÑšÖþ±G\ØšÑÆ¿unlIù…iœÆ‡–A­0¢BØz²¿/"W9E]ÄÄÊw}ˆ„÷Î}í<²ˆàÎ/³Þ÷núö¨Mä´¼²`N€Íé´xìn*d™Y‡ö’óœœHÜ@XxöÜkŒ*÷ܧ ñuçåÏ’pá[’©ð=ÁƒºðŸ77ÉÚUbÑÊÓÔl׎ú§2 èfZØV±:´óe×âp{b‘2OQ1±3áïñÔ[kÏS·gŸ¬iâs ˆóåcmÜ’æ²jsÊùãùöd6mIcTçÖ\wü+Âb^`@§Vø¬Š¥}Ç¢æ}Ì©Aѱ±•g:ps½½¬Y›R´óDÊ(E]ÄÄ\™Hˆ%ÿk›ËÅ=«Æu±kb|Ƽóë~MdlÏ;¸ÁÏ—Ûƒö²î½¥œì0Œ‡o©ÏGg¬ar¢½ ¥ˆ”y:QN¤ r$E—U[nmJ¹¢>È•MVøùûû…#çùêÒ®}Ãó§³5¢}ÛÚdÅG‘lw¼b‰uï¢[ÿûèxxëSÒømM÷ô¢÷½­H]µŒ5]¤@©‹˜˜¥ZÚuè€ûélΓûØ½Š¹ŸïbÁ>`‹6àL¥ U*`<íØÃÎølúuJŸ_`Ô£úÑUü‘^èÞub5óîfÁ°©¼~z?$4d4Ï4ÝÍüIadì]ÆO1£3¤{æ>A¢Ý‰sõrÿ9–A–=Ìy+5]¤`Šºˆ‰•k;œ9Ÿ ¿à:{ìt}l;¦$øØ8F?ù3†WÁšNjÊfV$ð|œÝuœÐ)“hÿÎF‡tÅ–™Âºi‘„F¤á8÷i¶O°Ó3x =jÀá¸5Ìú&s#sßr8RXúÝ&FµªÁ²Ðøœ€ï[ÎÏÑc¸Áº˜ï”t‘Âxz[nä›wŸ,n“Õm²år/}rçÝ/CêÔ©ÓÆßß¿d¶B¤Œp:8N‰ŒŒ,íÕ)TëÖ­‰‹‹Ãb±`±”Í#¾éé餦¦îFg€³ù.í¹óvÀá69Ý&W¾)Ï;ÊÊæwXDDÄ„u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u‘²È¨FÛ'Ç3ê¾k±–öº\-,µèØo,Áw\£N¹béÿ¦HYd©Å-½ûñP«ªÿþ²x`iÀ]ýûs@}ÏäŠe+í‘’`à×òïŒÛ{Ú4¢ZùlÒ÷ïbë!¼öá:—öÚUnÌ=†ñÔC¸áÚªXO$aó ¾š5›ÅÑÇqUyàÝ¥¼ý`M*XÁqö$év±æ;æ|ô_"Ž: X~î8Œ~u¢ÅµUñÉ>Brt8?Ϭµi9Ë1!E]Ä„Œê÷óÆÜ׸íÀwÌœ4•ÝéÕµ¡•3ƒ '”æ>w£JGÆÍŸÉ€ûøå³|óöjÍèü·§yãË»h;æ)^ZñN¬T©^ÛŽ™ ú÷ZNùørM£Žü}ØËÌïPŸ§{¿GÄiË÷»™1óg|ÝAV-šÅ¤ˆýœ, Mo¼…ªÏâúë7Yä/£¨‹˜- íª¤²xä«|²Åž{í ¾¿à^ê<ü.¡½êR·’C±«ùôÍ×Y°=#'|ÖZt<‘1ßAà5.Ç­á‹)o1/ü·¼±šÙ7~ÍßF¬ð¹ÉkfÒpFWú/LÉAµž³ {1›‰wŒâ猼ç¬ÄÍÿ˜Ì€ëbxÿÉ`æÆeå^¿ŠeßÿBä‡_óê¤ „mÇòôœ[œÇ’ˆˆˆà$ÀÆuüv  Ëf?Æ£AÓ‰Øp6ß–W¤í?&3¨Q"!O dæÎ“çnY¹tÑù»yݶCœÿ—¿žþù£¹™ë*e±oGÙþ<£­]†=Ïðî¨måèÎÌý×›|¡7R*tL]Ä„’Ùg¯Å­Ý»PÏÇûý²’–1}ÂPz“г™0}·ûT Íè9ÌÒ˜„yã žÀ'»š2töF´‚áÛȾ®5­ýsŽ.ÛšÑÆ¿-nlIy|hÔ #êw¶žt{Šxìáz^úŸŸ z®³{ùöÃoØWõnzÞéýXÿ™ã霤+z¸GÅŽôêÞ€£?ÍàÓ'ÿ|;…m[ÎÚcøqÛÄ9LéU“m3Ç1ø™ç™½1ƒ åÜŸ³mÇÎeZ¯Š¬›2œ>ý&ðUz'^œ9ξ޿ç"%I#ur¦|É‹¯4ãý§³ìîX–|ËW_-aCÊI·¤“ã;WòsXv`Ë ¹ý¿=èü6ÖÅÝEpŸ&ÄÔ—?K„oI¦RÀ÷êÂÞÜ@$chT‰E+OS³];êŸÊ€ ›ia[Å6èÐΗ]‹Ã9ìvÛZ» ×û9‰ß±“lëm$öÌ›6ÀÊ·[ ¬å}©Õøfþ6¦' ýÊûÛÎb«èKE[^h휩‘³ü„è²<,À¨Rð¶}úì22«ÞMßµ‰œ6€WìË9¿9ÝÁMyËñïÊÀÞµY?©3–ÃÄNªOç#¸¿Ýë¬ ;s1ÿt"—D#uSÊ&éûy´sWú½½‚ô–ƒùð—åÌÞ–*^†ÀŽý{Ùï¬Ju ÖÆ-i^á [6§œßmOfÓ–4*¶æºã¿SöZácøÑ¾cQó>fcÕtllÅÚ 7×ÛËšµn0 Àåò²sÚåÂåœço/ßõ="â‰ÚÊšïCx¢Ò2^ ~ÇÊqÛ«¿²eëV¶nÝÊÖ ïp—O!ˇB·­‘ ¬iâsˆˆƒ^Oª³^ß‚€Š•éòæ:¢¢£‰ŽŽ&rÅó´-W™š5}u†¼” ÔELÌuú?Î"âÇ,2—…ϾFÿ_ez‚‡û:8°`µGÉy€u¿&2¶çÜàçËíA{Y÷ÞRNvÆÃ·Ôg‰£3Ö09Ñ~ÁÃiɤœ²Ðö†@|¾Ûø§Ñº­é 4/o'%y?ŽÜ1Ç™ð÷xê­µdUlÍ÷_¥SF<[3qaññ3<µ¸\κ:xàzî9má¦À|÷¸7 ÐmrÞY { \‡X:q³¢Ý·ÓÅ©Cé:¦.¥B#u‘2ቿm&•4jPø½#)š¸¬º´kßðü‰ò¶F´o[›¬ø(’í’W,#±î]tëo`}J¿­I ðž^ô¾·©«–cÏ·àS¿óý/P«û3ü½q¹ o³Öã¡¡½htr-KÃŽŸ‹¢+ó ±±Änûš—ÇÍçãy{@sÊá"=i›6ndãÆlܼ‹£'ÃYºâ(µºáï=ŸLPø¶åݧ·ÜÚ”r—Žä8ve× ùuVö%%‘tnÚCj†÷Û‰”$ÔELÈçÆALîY‰ˆðì;rKÕ¦Üñto®ÏÚÎg*ퟹN¬fÞÂÝ,6•×OOã‡DƒæŒæ™¦»™?)Œ €½Ëø)fc†ÔaÏÜ'H´;q®^Nâ?Ç2Ȳ‡9oÅðçgÊä·©“ø¶ýt&~þŸ|Áªè?pVàö¿ä‰öÙ,Ÿø?qñçá´‹Ì-Óxaî-,ö*O­îÇ'»ó=ƒ+ƒ°©“ùéæ÷˜øÅç´˜ÿ5kbS9eñ§A‹›¸vߘòC¶íÄ*æ~¾‹CBø€,Ú|€3•‚hTéüJ¹Ž­dþ7C™<•÷ñí¶ƒdW¬K3ÿ¾[A¦†êR uÓ1ðÉ>NzûðÒ@T«€óäaöD­àíÁSY´ßY„Ï©Ÿfû´! ;=‘1ƒ§Ð£Ž[Ãì¡o272÷4G K¿ÛĨV5XŸð}Ëù9z 7Xó}‚ç7®£¿òê“ýˆ5’'L¢gJXÏ%þ÷Ÿy«ßL¾ÚzïãÜ,¢æ¼Á—,`Øøîü4ü;Òòôv¦…ò|¯?ˆ6„^ŸÀCµ+cÍ>AZÒ”ÇR”m#‹Ó|l£Ÿ|ë`ÍN'5e3+’NägÏ$ü ?6ž‘=_búÈÊ™J|è»,ÿE]J…§CKF¾y÷Éâ6YÝ&P.÷Ò'wÞý2¤N:müýýKf+DʧӉÓé$00ÈÈÈÒ^ËÀFàÈoù¦ï!Þ4†ù;Oè·½™LëÖ­‰‹‹Ãb±`±”Í#¾éé餦¦îFg€³ù.í¹óvÀá69Ý&W¾)ÏoËæwXD®vâ>}ž7Ã0ê¿kXýÝ\¦ îèõ })˜v¿‹Hé:ËÏ>Än¢S‡ix,“Úu-rQu¹8ÈHÞ̲äÍ¥½""W5í~1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEäÏ _u¡S3ß ÿ>³·ëmAŒùq#¡;Pá¯]Sq£¨‹˜’_ËÞ¼öéO¬ß¶“˜Ûø}ù×Ìu;×å§ÞÖ’àŒ¾½z¾x{¹Þ•IZò’žÀqY·CDŠC¥MÄ„Œê÷óÆÜ׸íÀwÌœ4•ÝéÕµ¡•3ƒ g <¡#‘…£ga ,ZDŠN#u²t ]•T¿ù*Ÿ,YÉš°|ÿŸ)¼±`;ÙçîT.£¦óõŠlÜÂê/ßâÉ–~n#pAϯ$.!„„(B¬èýzksF.‰"t\«œ‘‚¥÷½ô–¬ØÀÖ¨b¢¶öõ»<}£Ûò-5h?à¾\¹™¨˜vlãÇ/¦Óïÿl`øÒ¢÷d¾\µ…è˜l[ÿï=Z_/X"…ÐH]Ä„’ÙgïÅ­Ý»P/r%Îä¿G%ÚŽË´‡ö3÷_Ù”V•[‡¾Â‹3'°ÿÁ—X“ `'vÞ&,>ˆ'²rëíz7†?ÍÚ·£FâTƽ²“ìJ×qçà±L˜>=¾ÄÚÌ ´9¹ƒ*öák ßq£ÞƒŒÿW7ÚÔ³`uôçí—ï$-d<}צaÔlL•ƒ‡(‰ "f¢¨‹˜3åK^|¥ï¿8ewǶä[¾új RNâ ÿ® ì]›õ“ú0cÙ1\@ì¤út^1‚û۽Κõ9ËÉ:´‡„„”ó1-çåz«§µp‘™´‰u¿Ga'œM©õ¹õëîtþ?kcï`P¿f$Ïýã>Žå,`ToÈÓÎnÕªSÍHgý¦l=±1%ó1íÍ1¥l’¾‘G;w¥ßÛ+Ho9˜YÎüám©b€õúT¬L—7×Mtt4‘+ž§m¹ÊÔ¬™ïÌöËı/ÿsV¥º¿[ãÖ´¨˜JøúDÎz¸¯}Û"æn¨ÂÓŸ.eÁä!ÜߢºF "E Ÿs>@ijˆøq ‡ÌeᳯÑÿ×G™ià:ÄÒ‰ƒ˜mw§¥ãÊ™õ²Ð‹\‡¬ÀbÊ»·SåÏij`È}¬éô}ôç­ï‚yúƒ ˜‡ý"’K#u‘2ቿm&•4j`Á‘Ç®ì4¿Îʾ¤$’ÎM{HÍp€+›¬,ðó÷½ðEÂÛõÅäHÙM²£A7Öõ¾×Iö®_È¿‚aÐü#´~ªAå.áIEÊÔELÈçÆALîY‰ˆðì;rKÕ¦Üñto®ÏÚÎg1v\ÇV2ÿ›¡Ì žÊûŽøvÛA²+Ö¥™ ß-‰ Ó±‡ñÙôë6”>;¾ Á¨Gõ£«øy»—ë#‹·~®£Ëùò—‘„ŒœÂ §gºÛŵw>H ÂkÃ;y¼ƒ…„¸œ*WMüáø1Ž_ä^‘²BQ1Ÿìã¤×¸/ ¤Aµ 8OfOÔ Þ<•Eû@&áï `ø±ñŒìùÓGV†ÌTâCßeùé{ò¯' ´=‡Y}aéÔñLI4xä<;{úöfzT6àäpÄ"¦ŒGê ƒ:·óâ³ÿfBlWÆ.ËÀeÔ Õ]wríÞé<÷ÚVÒmþ8öÁiøqÛÄ9Ly8“ÅÓÆ19ÑNí¶=ÚÆ8ÿ†Å5 ?o¿|'i!ãé»6 £fcª<„Óí>YI˘>?œ?¨G—ÁÏ1aúö<øk3‹°ÞÖ“Æ€Øy#˜°ø NœdÈ*ÐjÄ\fõ·²bÆ |ŽOÍjœØg/â¿ëŸ—i áq¥lQÔELÌvÓX>íR™½S{å :þ]Ø»6ë'õaƲc¸€ØIõé¼b÷·{µ~eóÙWèr[5¾øæ® ­iÛÒIÄ{Û8E ÀÁÁUs™õß(ìÀºß)ßäGžéÓ™é!³Ê]÷iBüGÝyù³$@ø–d*|Oð .|úì22p‘¿Ž•¹ëû>Í\Ìc76Áºl;9‰sr"qaáQä%Ϩv7}{Ô&rÚ^Y°/'ΛÓiñØÜäáû`T«N5#õ›6²=öį仇“ã;WòsXÎslù£!·ÿ·ÿÏÆº¸¢l‡çõ$w`uh )çßDøÝIp¿¦$~Ô‰Ÿ$qAÊËQ$ù—i+t¥,Ðîws¥me]üYZ z•á·Ô¸àÞz} *V¦Ë›ëˆŠŽ&::šÈÏÓ¶\ejÖô…caüî¤m×ÎT3ÀÖ¼=AcX·þ(.OOæH!bû!*4mACX·¤y…ƒlÙœrþø½=™M[ҨؚF6®íú,Ó…²vÓV6¯ù˜¾×Yññ)_àvYÒÄç /m{cß¶ˆ¹ªðô§KY0y÷·¨^àˆÆ±/ûU©îo)âv­I+ZTÌYfQÇæ…)î6Š9)ê"&æØ¿œI÷b⺠üè?¼tgÍó?ô†®C,øÝ»wϺñÐóRh:.×1Â~\‡½ÃƒÜ]ÃÆõ·t¢Î®5ü–ê=£†a€Ë™}£Ð³³­™öþSÔú˜ Ÿ Ï3ozÀã[† ¹\9Ë/ê+Ø™x ¹GÌ#¾N/Þú.”ÏžiIo‹w:p`Áj)Úv¼®®3,Þ—éráp€Íf+à>®ó²R¶(ê"&çÊÞÃ’ }ùÇw=?˜ÇÄ[s>[îHŽcWv š_ge_RIç¦=¤f8éa‹øåÄÍ<Öãºt¹žÝËW‘ìí´yŸ@n½¹&'c£I±ƒ#)š¸¬º´kßðü e¶F´o[›¬ø(’íP> Í,[ù2d1¿G'ÍÞôÂÇÞ9Ë®Ç-·6-êÞjpdïú…ü+øÍ?Bë§zT„e;¼?g6YYàçï{Á‹­#9–Äìœeþi4í:Êácõ›\‡Çý^–ém¥lÑÞ‘²Ày˜°7Ÿá¥ê yçý÷ØûÄ>OZÉüo†27x*ï;>âÛmÉ®X—fþ)|·$‚Lpz_}›ÂNæZ¿Ý|öÂ·Âø5¾™Û:Vàt…t||ÄðÑ„µdœXͼ…»Y0l*¯ŸžÆ‰ÍÍ3Mw3R€mw<ÉF?zèAÚO µ×¡oáãb׉UÌý| †„ð3X´ùg*Ѩ’çÇZÞÉã,$ÄàT¹zthâÇq¼(;а^9ö°3>›~݆ÒgÇ$õ¨~t?G¬â?ß å“3ø·1“%;þà¬_}*íeeìÂVîbÄàqLŠñaI|:4hN5KÁË\vä&z{ØF)[u‘²Â¾Ÿ¥/熯>eÜ;Álí3‹ðw0üØxFö|‰é#+Cf*ñ¡ï²ür¢ŽøE ØÐÿ n‰œÇÒ=¹Iw¥“¸i+‡ïÂóªàcO'eÇJÞøŸÇäŽwšíÓ†0ìôDÆ žBp8n ³‡¾ÉÜȬœUŠÇs¯×âåÁ/2ë)_¬gO‘~8‰)' Ù˜,vLHð±qŒ~òf ¯‚5;ԔͬH:ñ§ãì¶͹wPžkXû þ·s5oMœGœ ÿ-<…o‡W®ã„N™DûwÆ0:¤+¶ÌÖM‹$4b›þ=aÇžãOLbƳ8{l7ËßþÕ±©ÄÎù'ÏW}…QÃßå¡*V²ObÿŽpb;½.ó×XÏÛ(e‹§·µF¾y÷Éâ6YÝ&9çlÚŸÜy÷Ë:uê´ñ÷÷/™­)#œN'N§“ÀÀ@"##ÿš'-˜IƒbäG<Ÿ$'âEëÖ­‰‹‹Ãb±`±”Í#¾éé餦¦îFg€³ù.í¹óvÀá69Ý&W¾)Ï?’©‹ˆ©ß¢)~T¦õS/òèÉÏèÿ‹‚.r¥SÔEäÏlè1y>#›;Ø·ù^9›O¿ÕED®(Šºˆü™=–™=obfi¯‡ˆKÙ<À!""bBŠºˆˆˆI(ê"""&¡¨‹ˆˆ˜„¢.""bŠºˆˆˆI(ê"""&¡¨‹ˆˆ˜„¢.""bŠºˆˆˆI(ê"e™Q›zÿ“á÷6(ü/ŠÈOQ)Ë,µèôä@º·®æñï0_6†/ ‚ºÐ©™oÉ>H§¨‹˜œQ£3·Ä1ÿIê•ÖO¼­%Á!3}{uE]¤)ê"¦f¥i¯§¹íì²Ûõ£oŸÒ^!)AŠºˆ™UêHßǯcã# Ù\‹žýîÄÿOCe Õoǂ囉ڹ Kg3þž†\k-: Ê7aÛˆŽÞJØ7ï3¤cÍóÇáËuäµß¢Y4°á¹Kƒ§ù2j¯v,—{ çW—@BB!V,¹í)£uÓ2¨~÷<ÀÏÌ_º>[½KºÕÍÿcoPÞç8æ¼Èðg&0{gžøàcÆw¨œ{{ÚŒžÃ¬!I˜7žàà |²«)CgÏaD«òÅX;±óÓ½[7ºu{”7×f]¦í‘<¶Ò^)!–<üÄ­ù±á']Ø×}Ňð÷žÍY‹ýÜ\5—Yÿ¬û=‘òM~ä™>™¾ñ2«ÜEpŸ&ÄÔ—?K„oI¦RÀ÷ê§Ï.#£ˆ«”uh )8Kb{ED#u³²<Âc7ìæ»Å19?É·‹wѸǣxhÝ‘BÄöCThÚ‚†6°6nIó Ù²9GÞ}ìÉlÚ’FÅÀÖ4ÒÐ@䊡GSò¡Íc=¨Øñ¡±Œw¿ÉY‹ž¦±5ì¤×G†.'®œ¯ ?cÝåÂá›Íæý¾®¢¯½ˆ\E]ÄŒ*uäÑkùáÓ¼¶ü¸Û Õ¹ÿõÙ<ùXgÞYó éžëÈ­7×ädl4)vp$E—õíÚ7ĺ=g÷;¶F´o[›¬ø(’í€õ(‡ÔoråIâTþeº²ÉÊ?_, Ýï"%DQ1!¿[»sO•|øßÄpO¨…ÌŸ£4êQºVåÛã~oæ¶Ž8]¡ÅÀ1|4a-™'V3oán ›Êë§§ñC¢AóGFóLÓÝÌŸ–s<ݱ‡°•»1x“b|XŸ šS-ïŸc;ã³é×m(}v|A‚QêGWñsDºð"—‘¢.rr:Øl6ìvûŸo4ªqç#wRqÇ4V¦æ;Ù¿ò¢ÆŽ¥ÇuYüE:‰›¶røî!|0¯ >ötRv¬äÝïñyÌ™ÜÇœfû´! ;=‘1ƒ§Ð£Ž[Ãì¡o272ï v;±sþÉóU_aÔðwy¨Š•ì‡Ø¿#œØÃNp'tÊ$Ú¿3†Ñ!]±e¦°nZ$¡éçÓ‹éX­VœNí—ù+y:üeä›wŸ,n“Õm²år/}rçÝ/CêÔ©ÓÆßß¿d¶B¤Œp:8Nš4iž={ÈÊÒÇÂäÊU¡B5jDRR‹‹¥lž›žžNjjê`p8›ïÒž;on“Ómrå›ò\°³«l~‡E®rgΜ¡bEýò¹²UªT‰³gÏ–öj”)ŠºÈUÄ0 à 33??¿Ò^‘ùùù‘™™yîÿ­”#r•2 ‹Å±cÇ8{ö,×]wÇ'55‡C¿ÒEþ:V«•ºuëâïïÏþýû9yò$‹E1/ŠºÈUÈýÅÒb±™™ÉîÝ»©U«dffrâÄ NŸ>Ýn÷ü›çD.’ÍfÃf³Q©R%üüüÎå¾{÷nǹ k”þ×SÔE®RùÞwŒÝf³Q¥JªW¯N¹rå°Ùleö7yIÉp:ØívΞ=KFFiiiØíös¿5NA/=ŠºÈU,ïEÓ0 \.×¹èŽ;ÆÑ£Gq¹ôçR¤ä¸ÇÛjµ^r½t(ê"W9÷ϼ¸çÅ\Q—’äpÅüÊ ¨‹˜Dþ¸çQØ¥$ä·b~ePÔELF/¶"e—Ξ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1 E]DDÄ$u“PÔEDDLBQ1‰’ˆº+ߥˆˆˆœWbÔH]DDÄ$.gÔ52)¾ËÖÏK‰zA+áò2/""RVµÝÍK©öÄ ºˆˆÈy%ÚÍ‹‰zQŸP£u‘ÓÄb·³¸Q÷ô..<“ÏÓ×"""eÝÅô²X ½ØÝïEyb}´MDDä«îí¸¼§õðÈ[ÔÝCîíº‚vEàáñy÷µr>æî#õÂŽ¥+è""RZòÓ.(êÞFížv½e¤î)æ1»ß½Ö úÍqy÷qº§]îîóx¸Ì?/""R’¼í‚Ï»Ì?Ú.(ìÞ¾övL½Ø»á‹u÷“â<ÖóFÜžâžÿÝGþ˜{Ûåîm„®¨‹ˆÈ_ÅÛ¨¹ óʼŽ Ï¨ç»§ç+Ô¥ž(—'/øyq/,ìy÷óu<\âåk‘’RXd Šzþ¸{ûú’Gèy.e÷»ûÉsy‘† Þÿ¤8÷“éŠ3BWÈED¤´yÛ_ЈÝSà ºù–[,EÝÓ®vO»Þ=ïî¼uß}Ÿÿ yÒEDäJUÜÑzA÷ôua1/ʉs@ñGêùãìíŒ<÷ »ÛEÑF祋ˆÈ•¨('Îeò¶ÛÝÓ2‹ìR©{ ;x‰{úµ³x¸Ì?/""r%(jØóíiòö˜‹R”hzº§ðz ¶ûü¥Æ\‘ÒâmÏtþù¢ÄBæ‹úœr±QϽ·@zO—E}>‘¿’·˜ö¼Ë‚^Ô t=PôP'ìîóŸ‚.""Wªâ„Ý}¾¸¿¨ CñbYÔà5ÜE=³]A‘+EQƒ[ÔpõÌö"g/n0 ºA‘.h4^œeŠˆˆ”¶‚[P¤ g™^]L4‹aÈEDÄìŠ;‚/Êc »íO.6¤—ë»KY¾ˆˆHi*Jp/ewz±?Úv)ñ¼Çã/e™"""W‚¢Æ·8‘¾¨Ïª_®ˆþUoDDD®dý‹c.ñ±@ÉU‘)šK¹»¿"ÀмˆˆHŽËñü,…ßEDDDDDDDDDDDDDDDDDDäêõÿ¤2dÜP;XIEND®B`‚OTPClient-3.2.1/data/screenshots/settings.png000066400000000000000000000731141452112020400211320ustar00rootroot00000000000000‰PNG  IHDR`•À>+æsBIT|dˆtEXtSoftwaregnome-screenshotï¿>'tEXtCreation Timeven 10 feb 2023, 16:33:19žãË« IDATxœìÝw|õãÇñ×]VÓ‘¶ŒÒ–)E¶P¶ N~€ˆ Ù 2e ˆ ˆŠ" |EA@–lhYEYF)«Œ2Ût&Mr¿?:,%mÓ’6¥ý<{$ÍÝ}î“»ôO>·@AAAAA(®$WW “¢TAŠ7ÅÕׇž«—/‚à²0vEŠÐ¡¨*Ô0.Œ0|eˆ°áA=H¨h TÀ9Zn^–/ÂXGå%8ÖéaììPs¤¼ì¦yyAÒ9”ÙMó óæ™3-§²ì{×Aa/,ä5GÆ9Ìá–—àÍéïܦÍËrA(Yr ĬãÇåVöqAí s$h³ _GƒX° éíRÈ.x•\Æç´ŒÜÆe« 8·`Íî1»×]– %S^Ã7§àÍ-ˆ¤ïø>ù °¼„oNŽL“—e ‚P²8ˆ¹…¬âà4yY¦Cò^¹í(s4píýÓüŽ,[„’%·gކ®½¿sšß‘eç*¯á•ŸðÍ.xsG–çÙ-G!]Nݹ…mÖ!»isZNN¯Ù¥vtBò¾¹®#ƒ½²r« %SNý½¹…mN!lo9R¦ñ’ií½fW^8'y \9—ײ–•yYŸçôš ÅS^ºì…°û×Þkö9k監œSà9¾²çö^s¤EœSA(™r ߬ƒÌ½k#5S²¾–Óò$²c‡:¿-`GÃ7kØÊ9<Ï-ˆí-×^A(þrjçÀ6;™ÃÔ–¥Œì‚8·Ε#œ]ßk^Â7ë`ïõœ‚;Ïs«§ ÅWnl/„íozøfÒô&íQ&!œk(?hpÖ.Ìák/hUØäò@à1 PÐe³Gê"BñæHk3ó4&à6p8 „Wù/„3‡qvåØì¼ö@}Áy ¶œZ¿Ùu7¨rxtsww¯Q¦LoŠjµZ¯R©4’$òTçP«Õšb±X’.ߺu+611ñ°ˆ¬üÂÙ=·×ŠÎÏajrK¹œºérPeó¨zéõúç*T¨PE¯×Z­VRRRP›Í†¢<ðFA$IB–e$IB£Ñ V«IJJºyùòå³III[%€™{C7룽 Îîøá̲ 3Unõ¶ó>þQà0’iG~†gíóut> çv´õ›[Ë7ó ÆTªT©‰¯¯oý¤¤$Éjµ:ZWA§±Z­X,ÉÛÛ;ÐÍÍM[ ØGÎ;Üò#Û@ÎkçÖúÍ.xU¤îðëíïïßÌÇǧARRRÞ߆ ‚“¥¤¤àááá§R©lñññzàX.³äÔç›§WŽpn}¿öº²†o5½^ß§|ùòÍ’’’Ä6AŠ ‹Å‚Á`ˆ×X,–£@ ÙêæÈN6‡2NÎ[5ï[€½®ˆ¬ÁœÂ]+V¬øHrr²_AŠœääd©|ùòU€.Ü»ÿÊ^¶Ù;Q,Ïò{1žœúƒíµ†Ë»»»W—$É_ìdÁYÔj5Z­FCú¡«Š¢’’‚ÙlÆb±8\–¢(¨Õê@ww÷Z‰‰‰Àî=u9óÑ™sÏ‘ ôد6¯g—èöús ahR¦LŸ”””û AÈ+Fƒ‡‡Æîx­V‹‡‡‹…ÄÄDL&“C妤¤PºtiCbbbà÷æXæÓ–³é! öÃ×n(;Ò‘Ûi¿ŽQG­V:°,A„lI’„——>>>Ù†ofjµƒÁ€Á`ÀÑ“»4M P‡ûÏÜÍî2 {NÚ¯ŸC5²/»Ö°½pÀý–%B 'Iƒ­V›çyu:’$a49×ÀƒÔ̲—e™[Àpo 8Ïò³.»ÖoN]>’$9ëÚ ”@žžžù ßtZ­OOÏ\§KË*_îoýf7dÌš×:9ÀY–[_pÖo ]~*'”<²›/kÖ @Ÿ‡qM±,„ý[çÒýQñ=_iµZÜÜܸ777GB\"5³ì]±Ñ^øf—‡¹zÃв.ÌÞ!G?ojÿ–ôŸ²ˆµ;Ãø÷è¿„íÞÀÒÙïóruÇÿi$ÃË|²›ÿ›@û•ÃãTþÁ‹ç|Čͱ~Ÿ¡HQ«Õ¨Õ÷ÿ²‘e™éÓ§Ó¾}ûûƵoßžéÓ§Û áìʳ#·pº|‡qnµÈ­@GK{ào ¡hS×iC›Ê$k‹ûõâ˱àU‘zÁ¥¸r&Óµ>T~<þæ(†½ñ µüTÄ]>̦3øzõ)2§¦º.#×e$rp íúüÂõÆ]S7 Ë¨¾¼ì…²g3ÓŽÑyâ{t¬WÿÒÞx¹)ÄEcëÏÓùò÷pâÓ—¥öç‰ÞÃðÚSÔ)ï É1ܼrž“~ŇKŽcFEÀÓC;¼MƒJ¡NºËõÈ|9t*Û/,ÙuØl6¶nÝʻヒZ­fõêÕ¼ð Œ=š¯¿þ›Íþe´ZmnÇ;’gŽd[Ž;èòÚa–]‡sn;ã„bL‰3b¤²ÔjÞˆ*á;9w™Ã»/gšÊƒ&£æñ}ïjhÌ1\‰N¦T•tÿ¼¥M¯óî†[™ Læî•kĤ€åš‘{.×”Ó¸tª@‚ŸmFmOSüb=)]¥>þÛ•—™°'$ožühßv¬ˆÊKTÄYRÊñHͦ”kZõ’㤔}…§àiw ±—#¸”ìI Ÿ–äx¾…)§ÃÍÖ¯_À»ï¾ ¤Çûî»ïòõ×_g²=yhçuç[žŽˆpÆ=á b¡˜²žYÊŒ¥Ï1£GMš ø†Õ½opbûj–ý¼˜µGï`är/2°sóf½Ñ‡ùü;}ÇêOZòÜmðÛ´”F°èínÌü/^¥ÆeËv…åƒ^dÚ±rtûñOÆ>^ŽgŸ{Œ){öc-ÿ2ý_®€Úv•?G¾ÁGÛŒTü+†Ô̘]ö«D7 %i?_÷Ìÿ®[‘Õj$ÇO®œ ·ãwׯ_$IŒ=€¯¾úŠuëÖå8ON}Çé‹ÍaÈ: ä3ãô0´¬É®;B(Δ»„|Ñ•—z~Èw«öqÑ\š:Ï÷gâ/0뵊©WcªUŸ:n’®>#Wâĉ£lýôI<% U@ òw’õ:GŽ]džŒwY_Ô€ºzmªk%l1¡¬ß}×n“Ź›]Ìàþýµ‰_¦¼MëGô9Þ*W(VrêvÈ.óÄ^ 8»Br{ÝÞ£àÃLôá5Ì9¼†ï§T£Ó”y|ÔʧßêH­Õ39#Ë©çr&ŸbÓ/;¹˜©«Ä⺠P”´ ”‘³~jrç‹ÙœúDV¥} åÔG%‡ß‹Iÿ0³woνÕž"øå¡ÿßsÌy³;sN˜ó^ !_²ëÇM—¹Ï7½ "sŸp~ÊLc/|í=ڛϡӑósW䬕ËZ¡¼vR ;Uyšµ "öð?œ½ž€ÕËÍ;ÉHxÈ`‰8E„åE‚5¥Ð^ÛÈ‚ßÎo­oy|ÌW¹aLqÄ›pàѪHçŒHj5X,Ør—*[#OiiO=ß–¼Ú¶"ÿnI¿L–ÓÔÞøÚÂYõÕpV}_ƒA?ýưzÕiÕ²sODØïœÎb± ÓéìŽ{á…ìöù¦÷ gÂ^¤'§–¯#—k°£}Àù Q¼%„ªf'ÆMíOJA±Z±¢B¥’+×víä¤l—W3mWfu¨ÀsýÁîwbˆWôx{šX3øY>Úm†”ãì?œÀ OúÒnúß4¾eBŸ¸–¯}É‘œÆå£ÎÖK±xkO¦¶  ýÔd>)ý_S]§?K~îŒÇõ(¢ãµVSOTÔ-Ñ QˆÌf³ÝcweY¦U«V÷õù¦÷ ?÷Üs¬]»Önk×lÎÓ/˜üæ_®;ãœÙófo¯ h—ª„ãl^¿Ó׌˜‘l‰Ü>˜?ަߔÝÄ)€r—퟽ÉàYk8x!‹Þo­™è3ǹfÖ¥~Hl7X=áCî>Ç]‹žÒ¥u&´ê\Æå‡íÆõfĬlÝ{°Ml>r 6s 6@-Çrñœ­_5«û#Eeýì‘LÚ#Ž5.D‹Ån‹Õf³1fÌ»;ÜÖ­[ǘ1cì†oJJJ~[ÀØyþ@rëH¶wH™Ä½·Rg4™m¦Ço«V­ZßY•„'áPùV±)€¶ =æüšë‰œ×•³Nˆ.†"D«Õâíí픲bbbÈíÒ¸‘‘‘G€¡¤Þ)9%Ócú`É4d¾{rvwLN—ç>à!Z¿BÑ$•¦Ý„µ|Ô0‰Û·ãÁàGY/ $fÕÚS"|‹³ÙLrrò_")))×ðÍ¢@3¬0®\"BX(zd’¯åüÝ Êú£1ß!rß.V|ÿ5Kωø-Šâãã‘e9ßWDKII!!!!/³xvå'€é ¡+mÖ‹¬ß›5®®‡à0EQ0xzzæ¹%œœœL||¼#×ÎŽ#Y—§³ààÁ/È.‚PhE!..ŽäääoI”.¯·$ʧ|g¡¸xª ””bbb2nÊ©V«3N/¶ÙlX,–<ߔӜÀ¢E,B¡Êîµæ´¬+ˆ3ðE ‚P9=Û ò("ˆA( ,ËĵzA\D° ‚‹ˆApÀ‚ .RhÇ'&&Ö¢A …ÀeÊ”)¬E ‚ <ëׯç>‘ˆ.A,‚à""€A\D° ‚‹ˆApÀ‚ ."®,%››ƒN€$ε³EAQL&SÆ…ÕK2À‚P‚xxxàëë›qñòÂ&I’$¡×ëÑëõØl6îÞ½›×{µ"€¡„ȸ…O¹råèÚµ+7ÆËË µ:5 , F£‘ƒ²lÙ2nܸQ u’e™Ò¥KãååUh'?%"€¡˜S«Õøûû#Ë2žžž¼ÿþû4hÐÀn·ƒZ­¦T©R´iÓ†Ö­[sèÐ!¦NZà-T­VK… ¸~ýz‘¿3‰p‚PÌ¥‡oPP?ÿü3 6t¨ÏW’$5jÄ’%K¨V­Z×S–eüýý |9E‰`A(Æ2ÂwæÌ™y¾;€N§cÆŒ@ ï%Ë2¾œ¢B°³H>wFÿVåÅJŠ„ôÛ¶{xx0mÚ4T*U¾ËR©TL›6 'ÖÐ>Fƒ»»{/§(Yá,RwèLÛZÞÅçfx’õZÒ4ȳø¼§¤T©R(ŠÂ‡~˜¯–oVnnnŒ3EQœP»œ•*UªÀ—Qˆp“ð¬õ*C†t智•ðÕš‰½vž#›~dêü½Üvuõ ‚º6=¿˜J_;sð\<9ýÛIUy®[o:µnJÍòÞ¨£‰8¼ƒU‹~毓1Ødºü°’Ñ 4ö 0ïeÒK㉹”OZ—A'ƒ-%‘Øè ß»_ýΡ›Öy›Å‘^¯G’$üüüvZ¹7ÆÏÏ›7o:­L{dYÆÍÍ­Ø',ØA’O+ÆÎþ€Ç¯­å§©ßsÞ(á[¹.µmqÄÛ(Ñ¿%$ï&ŒøîKº\fóoó™|êVŸª4¹ þø4 >Èç[ï°õË!œõ’Õ:MàÚaLÿüO.Ú›‘‹q2Oúx£:¶€áß„’¤ñ¤T¥`Ú÷Æ·Í«0º×4öÆ|ë«8ðòòBQºuëæÔ“,$I¢K—.Ìž=»ÀOÞðòò,¤R?Úˆ`Ïhþšñ¿þ›v˜LèÖÝ3•Œßóøý•r”Ó[¸uv7˾šÆòãi­G¹,M{¿Ãà×Zðh)¸}6”?¾›Å’°8šŽý“­æÍ?pÆ h›3nít*ÌïÀ7±!áóÒ ÖŽ21á¥ØŸ¾H?žù o=Dy?ô$q#ërÕ´|k}ŸoDŸŠ»á;X2c„'àó̧,™XƒÍCßâëAýÝæ, Ç­Éôú,PóØ;«Øÿ€‰mcÿ÷7gþ§ÐÓhÀ8º”?Å·†³ä¬)íõl]¿…Óðþ{#Ø}ðc¶9Lj»Iõi3JÕë„ú‡ðô†­”ú³Ó{ž£G‘ðÏ^BÎjX>ÿ5^j6›½[“œ¸U‹/­V @£Fœ^v“&Mœ^¦=égég%¸Ý–7Ök—¹b-C³vO Í~:Ó…­ÌýlÃߟÁ¶”挘:‚æ:êšÅŒÞU8·äSFŒø”_ÏW¥ÏŒYô¯ ÇÅT±6u¼S[ª*õ¨ë­¡zÝÚ¤.NK­zµ‘Âòï=ww2Pµa0¥"aüÈ¡ 7›Ö™–«'xÈ,¦¼¢gÏwc0è3V›2êË4÷T¸»s_mó¢Ó}yL¯¦r§÷è_a/3glå– À™_FЭKºtéÅŒ½&îáÖ„Ûúskã|~;›e\ÊeÖÎ_C”÷S¼ødþûƭɉ$+j´ÚüïD*‰EÁ`08½\ooïŒSŠ ’«ÎÖ+L¢ì [ÔLšR•‰£¦²â©Ó„lX˪•ë9•˜©oÔFì©l9‰8|«/|5Ôì=û=:Uáì‚îLúí6ààá˸-¡g–,y€“ ¦áczVíL¦Lp0‰ñ(õRC½“#J‚=ˆ\wÛ¶¬µSH¼pˆ=a'±r¢h¶ ]êr#ž¦û«~ìŸ6ù[cP€3Óüi±²ÿW:{Bï²cæWlÿå3ÆŽõ&î‰Êìò1[oÙ ­»ÖtëçÎEqßbÙ¯2•=¬Dœ<ƒÙÎxKÄ Î˜ºSý‘@dbp¬WƒV§Ã¦ñ l•†¼8¸Õ’ÿeå¿â¾‚Ž’$ EQ2Îps¦‚(³¤kÒa&.¬›HÏ­óyìév¼ôj/¦ÿÖ—ã?}Èû þ%Îζk—¹nóÆÇ £ª\‹GuÑì:”)Ȭ—8tø&ýŸ¨MŘ•„žI·¦µÑî:Cƒ&Õ8ùËÏ$öhCãÊ2ÇRÑ( Š={.Û Â¬Ë½–¾ÜJ5rw'ðãu„|”>…ŒZc#¹Œ;f”»Û™5»-Ë'¼Là¾ÉŒÛz+×eäãí_ÝÓŸ°9ä“Ô?”bÏ…ðãÓXuÍùµò®0Ž‚()Dç‘’|£›rtÓrVôžÍƒÞ§Ûḭ̂3­ÍŠ• åA¶kì ‰dðKOPÃÓƒ]fï·›Hlԇ盲ÞÚ‚ ë¡|™{R±Zþ[®$í6' cÑ©ÌóÚH¼eLm½K^ÔhX·Ä¨ûO®á(ÇÂÎvó—U4¬UíÚ0²tB ªE5] —/\q8ÔSÍeø7{I0'a¼ykw“ à ¡xKI‹Åâôka*\‚¾øw²˜$Îí;D´HÅÀÜû&­Ã9k*GpÃL'j¨*Ñ ¸,¦³'¹dµqqÇ6"Ë=ÅóÝZÑøÎöGÝd_è9ª=ó ž©MôÎmœÎãgßzù,‘f_­¤âÊ… \È.q#ÞHx?1œ÷ÛÜeþ Ì9ûƒ?xŠ2 $cJƒGö_IY·ùeÚõáõ ,;MÔåiÿÖ+TŠ aýîØcËÌw™“ÇO~&’+"|ó%=¼ŒF£ÓË.ˆ2í) ,ZÀÒÖíÁØ—õ;xš+w“ ðD—ר’tŒßHE%nKV\à‡>“›<— ç$ªµÈ›Uϳ|Zñ—¶ñ÷™þ îåÇ¥%ý8gµ¡ìÞNäà!ô.±øëSäµí¡Äìdùê7ùºçd>·.`í‘ë˜õå2Dñ×úc$x4eÐè6íϲÓg±~1ç âÝWBùç%Â#ÌtnÛ‡7NüN„äï]l>f̦ øa ¿Õ›ÊðæQ}ùì:u«o5Z¼Ò…—k'±ùÓ¯ÙSüÿ™Š“É„››aaa´mÛÖ©e8pÀ©åe§¸‚"€$¡I‰ÅXªÝF÷ ÐÇ [âm.Üά‘ß§öMæú["™ã?¼Ãèä‘ îõí|áÎÙ=,5“Å'Ò~¸Û¢Ø´öj•bëÖ³©;¬®lgKø`jªþbýÙüœˆÏÁÙC;Œþ/fjˆæìÖoض!œÀ®Ãx‰µ¼³ôtêN´ ¿3kÅËüØOn™À¶o§Òð“! øâ4 —Ù;÷$[î `Pbö2sÀ[ïÙ‡NíóÉ[^¨’oyx Ó-äÏcwE+¶ÅÅÅáææÆÒ¥KiÓ¦ÓŽÙU…åË—;¥¬ÜÄÇÇç>ÑCÎÞV‘²<Ï<È™U¦AMê>s5 M{žùñ›ºuëÖ/˜· ‚=H’ĤI“œv<ð?~|Æ…Õ êd ›ÍFTTT”íˆãdžf %Ë£%í¹°fl™%Ë‚¢XŠ©˜˜&Ožì”ŸóIIIL™2 @ÃàîÝ»VvQ"XŠ©„„RRRHHH`ôèÑX­ù¿–†ÕjeÔ¨Q$%%ø)Èf³¹ÄÜ¢H° S’$¢(DDD0tèÐ|µ„“““>|8‘‘‘…ÒõP’nM$XбÌ!I·nÝ sè/EQ8pà]ºt!""¢À»JZø‚8 BŠ=‹ÅÂÕ«W)W®ñññŒ7???ºwïN³fÍðòòʸYgJJ F£‘ýû÷³téRnܸ‘¼é×f(ˆ6›Í%.|A° {é×…ˆŽŽF¯×ãëëËÍ›7™9sf¶-áÌ¡›¹åëì𷥡ØKΤ¤$’’’pssÃÃíVk·k!sà:3|Ó¯¢f6›‰ÅdÊzòzÉ"XJôMNNvh‡\AñPÒ‰„(=Xsê‚ ž`A(ÁDк–8 MÁED ‚ ¸ˆ`A,‚à""€A\D° ‚‹ˆApq° ”@nnn tºÔ©ÖñÀé§"›L&âââJÄ}ßr"XJ|}}3®lVØÒ¯-¡×ëÑëõâb<®®€ …# 㲓åÊ•£k×®4nÜ///ÔêÔ(°X,F<Ȳe˸qãFÖI–eJ—.———¸¥ ÅZ­ÆßßY–ñôôäý÷ß§Aƒv»Ôj5¥J•¢M›6´nÝšC‡1uêÔo¡jµZ*T¨Àõë×±X,º¬¢Dì„„b.=|ƒ‚‚øùçŸiذ¡C}¾’$ѨQ#–,YBµjÕ ¼ž²,ãïï_àË)JD B1¾3gÎÄÍÍ-Ïeèt:f̘APPPÔð^²,PàË)*D;“äCýoÓ÷ÙÀìW¬ìÏ3Ã&óáË•ÄÊ ”‡‡¦M›†J¥ÊwY*•ŠiÓ¦ááááÄÚ§Ñhpww/ðå"Ô1xÙfVŒl„îAË’ÊФcwÚÕñ!Ûx’/už|šúúì§)H’õZÒ4ÈóÞåÛY’gM:~òk¶‡²ÿÀ^~é„¶ú›,Üü'c[z>XýóP!J•*…¢(|øá‡ùjùfåææÆ˜1cº¡çƒ*UªT/£((¾;á$oþoÂ2>i]¬`5'r÷Z$'lcõÿV²ûRRêt¶Dn^ºÈÅè8l®­qáPצçS©ñkgž‹'ã_é¾õ ¦NŸ‰Œlt>ÊáÛLÑ—°ènqéÂE®-<п¡ÃõòC¯×#I~~~;­ÜÆãççÇÍ›7V¦=²,ãææVì.¾Œ /oTÇ~bøwû1i<)S©O¼Ô›i¿¼Äš‡1uçMl¶s¬x¿+\]]W˺䂸½í3–í<Æû¥ÿâ“þ^=„|ñòòBQºuëæÔ“,$I¢K—.Ìž=»ÀOÞðòò*ö\ì» l±8úï¿ aósù´ß›LÜkà•Þ£m äjôû5„߇ÖF yP½Ã8æý¹Ð}{رa9_ H[Q2•^›ÆŠM;Ý·——ñÕ ”½g-Êø4Êl%dÏ.6.ŸÁ°g* Í©’êZø‚…+7³+d«ç§c-;?ñe?žý¿¬ÜÈö}ì ÙΚ…èZ7Ó´rYšö™Ä¢µÛ ÝΚEéݤL– ­æ±wV±?,Œ°°¦¶v»= ÃM§¢b÷ì #,l/‹zUFØ•y¡ÛL“¶¼Ò4îõ9 ×l'$t›WÍ¥o=­ƒëÊ‘z8ðžr[/9nÓâI«MýÄ5jÔÈée7iÒÄéeÚ“~–^qVŒ[ÀÙ°\eÃÜtùµ¯<çǦßï-uå“w[rcÞ' Ü{¹t<£o§ýV¸stß~ü 7â$üšôdô Ïq¦oKÿ-¡ÓIJñDDËTiû'ÏB3¬3þI´S!=ÁCf1¥ÍuÏÃÔhošõè/Gpµó$öÄgžÖ@Õ†Á”ŠüžñSNar¯HËžC1ußhNÍ IDAT˜ÄžuÍbưiΧ| AíòöŒY¸ ìËÜ“éw µpæ—1|úW46⣳»3­««Ç1fÙ9¬@òí«ØîÙ7¢£vÿ¯™ÑUÅöyù>܈¶Œ7qW,®+GêáÈ{Êy½ì Èi›_Š¢`0œ^®··wF?pA¶‚]u¶^a*y Ø.Ÿæt¼Šæ•+"sÏ8ÙÇÉÈþC9~: 8i¬B|Ä^v¦ý~:Ž Ö‹yñ±*¨¶Oû™n%z×®>‰Ø{àÚG–òf§üøÏîÉS@2C¤¹Á +õÿ\וƒõpè=9"›mZ\¥‡—ÑhtzÙQ¦=%!€‹} Xö©JƒF0k<(U©-_|ÖUn³úãilºe»ï+H®ð$¯7’‰8{$?1€1£ Ì"¸,u¦C߸¹9‚»–rxd * iÞXG’®¼qâw"$|ïìb󉼯[%f'KW¿É7ý¦ò™´€õÇobñ Dm !¹­+ëáÐ{ÊENÛ´¸2™L¸¹¹FÛ¶mZöœZ^vŠû!hP¬ØJ|l,¶¦}˜ù}læî^?ωý‹óQ¦1²Ð”ªÆ³=»1¼‚­%Ž+á»™5áÎXA9½„O¦—aL¯Q|ÕÅUJ"ÆÛ9—ÖU`$òŸ#Üyº7“gÐX\9¾ƒo†}ÇŠÓi;ÓlרðÓRžü Ã_ÝÁ¾ÙÇ88{(cb‡Ñÿ¥ÑLíïñÑœÝú Û6ä-€!™ã?¼Ãèä‘ îõí|áÎÙ=,5“Å'Ò~l+±lûv* ?€/žA“p™½sO²% úfïÆgÀë0ím–»Ø>ë»7å²®®‡ï)9mÓâ*..777–.]J›6mœv´‚¢(,_¾Ü)eå&>Þ‘¯×‡›½­"eyžy3 ªLƒФ=jÓžg~ü¦nݺõ æ-‚`O`` ’$1iÒ$§|àÀÆŸqaõ‚: Íf³U e;âøñãG€a€HÉòhI{n¬™[¦AÉ2¤»§IUòú€¡„ˆ‰I=ÄròäÉNù9Ÿ””Ä”)S 4|îÞ½[`e%"€¡˜JHH %%…„„FÕšÿ>«ÕʨQ£HJJ*ðSÍfs‰¹E‘`A(¦$I"::EQˆˆˆ`èСùj '''3|øp"## ¥ë¡$ÝšH° c™C822’nݺæÐ!^Š¢pàÀºtéBDDDw;”´ð…b}„ zâÄÕ«W)W®ñññŒ7???ºwïN³fÍðòòʸYgJJ F£‘ýû÷³téRnܸ‘¼é×f(ˆ6›Í%.|A° {é×…ˆŽŽF¯×ãëëËÍ›7™9sf¶-áÌ¡›¹åëì𷥡ØKΤ¤$’’’pssÃÃíVk·k!sà:3|EAQÌf3±±±˜LÅõdpLj„$=D“““Ú!WÐG<”t"€¡JÖœº „‚'XJ0´®%CApÀ‚ ."XÁED ‚ ¸ˆ`A,‚à""€A\D,EDRR&“ «ÕZ"nHYX$IB¥R¡ÓéÐëõ®®Î=D ‚‹)ŠB\\jµšÒ¥K£ÓéÄ N¤( &“ £ÑHll,ƒ¡È¬_À‚àbqqq¸»»ãíííêªK’$áææ†››±±±Æ"³®E° ¸PRRjµºÈBqçííZ­.2·¼,.d2™ðòòru5JƒÁPd.ƒ)X\Èjµ¢Õj]]E«Õb±X\] @° ¸”¢(·ú ‡,ËEæ(±åA\D° ‚‹ˆApÀH>wFÿVåsÿ@H>Ôïð6}Ÿ Ax@âH© ;t¦m-or=?H*C“ŽÝiWÇ'÷iÁÉ€:ÍiôˆG‰ÿ ‰.tžµ:ðþ·ËÙ°cûöì`Ó ™6 9¥ÅÖò@òmÍÄ¿¶±á«W(÷0}vÔ5é2ao5qà ¿˜§"2ɧcgÀã×ÖòÓÔï9o”ð­\—Ú¶8âm®®ðð©Ò¾#MRî’Tïu:ÔZÏ'R\])!¦ïÍbAýh#‚=£ùkÆüºa'{Bw°né·L_~œŒssÔ´ð WnfWÈ6VÏOÇZži­™J¯McŦ„îÛËÎËøjP ʦoI¹4-Mã§Øº—=Û×2¥}ÙÔ -—¦q¯ÏY¸f;!¡»Ø¼j.}륟 ã÷ü~ß¼›=!ÛY³p]ëzfÓB‘ñi>”þØJÈž]l\>ƒaÏT@‹-?ÙÌž_û$ÿ7mPß_ ]û. Å×½óèòêKåùwÁx-C»×[à•ucÉ¥hÐõ#æ,ÿ‹¿7o`õÒ¯éUGãÀ¸Ò4ê1žþ·ŽÍ›ÿâ?|L·†¥ÿ MCFþ±™ïÞøo?€Бoþþwh@.ËSþbþ¯+ùëï­lýû/þ7gkgþ<©¨3x)Ûvì`ÇŽ¿™ð¬[®¬¢KüK2ëµË\±–¡Y»'8¹“kæ¬Sè 2‹)m®³xƦF{Ó¬ÏF}9‚«'±'^áÎÑU|ûñ/܈“ðkғу>cÄ™|¼-_j?Ù’ò—äÓiÿbTyc»x:j÷ÿš]UlŸ7‘ïÃhËxwå¿3‚L¶2wùAn)þ´ì=œSGpñIìIÈZG &–ý‹'² Z¦JÛ·8yša½ùqß!L­Ó°ÌBÎݰäÃcõ+c>2ŸSEãä£b@Âû‰Wx–í|¾å$áwvÑó£Whå·›?£ÓFé¨Ù{*“;©Ø½h:?ŽC[Ú@Ü5k®ãj½5•ɯÁÖù“™{^â‘¶}è7ù tï aá©û>°vxñHýzøžŸÏäg0¹•§y×¼ýÙÛ\ê=)V"þ÷!S6ÝDÁF|´ýSƒçÏŸÏO?ýÄÞ½{íŽoÞ¼9}ûö¥_¿~y_E€àBf‹úƒISª2qÔTVb/;ÓÊ ?GPëżøXTÛŽ“šq6â#vkú‚=Ÿ¤gçG8· ;~½ðßëö;ÈFì©lIçð­ <¾ðZÔP³çPÖä´½k W§N»÷À9´,åÍNÍ™7u7‡-ïÑòq~_sEW‡úµlýî(I´NK9€Ö/7æî–‘NR°XËÖÛ3y±]UÖ.ŠHݶžÍéòz.,éËÔß.Ý»½s'y¶ Ë«•‰\Ò—/W^Â>…û#óèÒ¹9+>ÛI¼C•TH¸t„ýÿœÂÊaþ½éOãïZóø£jœLÂtû2ç#¯’SÏÛ—_~ɤI“X¼x1«W¯¾gÜóÏ?ÏÀù裪QQ$º ‰ ë&Ò³ý« žµƒ¸Z½˜þÛ|×7/ T•jäîNË×Jhh(»W¾C°ÆÒeÜ‘ÐRþéA|ñÓ ÖmÙÎÖ¿fÒ©¢­V—ãRÕUjSÝ-šEÝûϘ ÛµË\·yãcpà#b‹âè±[è©Nù¸¶´RÿéøH ®Ö€zúÓìÛ—¢qòçÃOõHž¯~‘›Î¤~ᦄ³aã*µiKí´^u¥šTÓÝàè‘«÷mïœÆ©*Õ (m\F0Z£ø÷èMtA5© Ê_mׯp]1àmÈÛn·S§N1lØ0:vìÈÀ3®ãÛµkWzõêÅðáÃ9qâDþ*Uˆ°‹(É×9ºi!G7-gEïÙü8è}ºíîÁO’¶Ûlœ0ŒE§2ÿ{ØH¼eD êÍ”I±þùŸ~Î¥¯}:‰§s[ $çi³b³bAFåàW´$Ë ØP”XB6îcÔÇ­yªÔFŽ7mJ¹È-ì‹{CCíçÛPÕ-j‹·10ó([iÚ5ZÀ±}I9oï? RîŸÅ†Õ *µãñ¡Ø,X‘Q¥ž‡oã«W¯2lØ0¾øâ ÆŒ@ÕªUK¤Ù—G+©¸rá2†K܈·¡«V›ªÒ¿üñã:ÂÂÏqîL8—¹‡›õò"ÍånX|6b²§}”f K“x6œ(«‚1d[âòb»&<Ѳ2ç·ïâ¢È_çÐ7àùçJ¾x4ýúõË4¼Ë/§ <ù|3¼$°FEp>ÅzõïÛÞ9Ž»tšs¦Ôqá ª@ýÇÊbŠ>>Œ9ò¡_-àB§­Ûƒ±/ë9vð4Wî&!á‰.¯Q%é¿¶ Äìdùê7ùºçd>·.`í‘ë˜õå2Dñ×úc$]ˆà²Ô™}_àææîZÊà‘ûÇX‰ÙÉÒÕoòM¿©|&-`ýñ›X<Ñ_ÛÂΈ¼¾ * iÞXG’®ÁqžÛÐÒ3œŸ×&âž_2‰ÛNÓ¹O[ZúìdÃÝV¬ëΗ½&0VZÂß'oañ @}!9ÛÃò?/2³ÇxF'/`óy‰ªmߢ{•‹ü>kojÿ¯íûB.Ò»kFE¨ùû\TÅÛÑ4µ^æL¤‰×žëN‡“«9/•Ã;&”í'ârl'%%1vìØ|¯»¢Hp¡’ФÄb,ÕŠn£{èã†-ñ6—NngÖÈïYuÍÄspöPÆÄ£ÿK£™Úßâ£9»õ¶m8Füé%|2½ czâ«.ž¨R1Þ¾Èñ¨œ?¼À¡o†ñnìp¼þÓÞÖa¹{í³°;Ol$òŸ#Üyº7“gÐX\9¾ƒo†}ÇŠÓé{È-œ]¹œ°.ci|â6‰æ¯sHÞ<ÞæqÜÂz3ë:µq=t§ô§Í3~lZÍ‘¹c;ˆ>/äó¾:,1—Øõý?쉸™ã¸“ >`\òúuG_¸sn?¿ŽûŽe§ÒT°±l_FðfÏq<ç%cŽ»ÃµðCœ½ëÀÅÈ鳨÷aú|ö$êÄ+Xpйpqdï;KÊò<ó gT™5 I{Ô¦=ÏüøMݺuëÌ[Š$]}F.ý’Àyyoã÷å¨[·nQ¹reWW£Ä¹xñ"eÊ”ÉvüñãÇÃ3’åÑ’öÜX3 ¶Lƒ’eHwÏ¿‚h NäF@õªxJîÔyc4í–3d«_AÈŽ`ÁyT•yaÜúU³rõð>§ÅÙ±‚-À‚óXOóSïgøÉÕõ„‡„8 MÁED ‚ ¸ˆ`Ap!I’°ÙÄaz…Éf³eœÒìj"€Á…T*f³#WœÅl6£ÎÃiÔI° ¸N§Ãh4ºº%ŠÑhD§Ë×IÔN'X\H¯×c±XˆuuUJ„ØØX, nnEãðE£.%˜Á`Àh4b2™0 hµZdY´œÅf³a6›1X, ƒ««”A° ¸˜$Ix{{“œœÌ;w°X,(Š8ÐY$IB­V£Óéðððpuuî!XŠ77·"óÓX(âwŽ ‚‹ˆApÀ‚ ."XÁED ‚ ¸ˆ`A,‚à""€A\D° ‚‹ˆ3á¡J?5W£ÑÚõqÓO±¶X,$''c±X e¹E•`A(A´Z-îîî.» yúr5 EQHLL,±×D,%„Á`@¥RàëëKëÖ­©U«z½>ãu«ÕJbb"§NbË–-ܹs§@ë$I¸¹¹•Èë"‹„bN–e¼¼¼ewwwºwïNõêÕí¶‚Õj5ƒ¦M›Ò¤IΜ9Ã’%KHNN.Ð:ªT*|||0%êMb'œ sƒY– ä£>¢FuAH’D5?~<*T(ðzJ’T¤®Õ[D B1f0$‰ÀÀ@Þyç´ZmžËÐjµ >œÀÀÀ¨á½$IÂËË«À—STˆÎ+ÙŸg†MæÃ—+9gåÉ~<;rßãAˆír­V‹J¥ÂÝÝ¡C‡>Ð]6dYfÈ!…r½bµZ¯/ЇÑÃúÙÊ;ɃÀz-iäÉíÿ•|©óäÓÔÔ?X9 5nJM?“Ês1g­çí?ÔCA¬ç"A£Ñ I¾¾¾<úè£N+·fÍš”*UÊiåeG–å"sëø‚ô°LÕWºòxÊLÁ]èT§düD„üHï*hݺµÓùmÕª•SËËNI¸=ÓÃÀú&tz­"ÿüðó—á¥.ObÈü¹Ò4áýõ¡,è^!ãMÉ]™²ž÷kÒ^QóØ;«ØFXXS[§m`¹,MûLbÑÚ턆ngÍ¢‰ônRÆñ•ãÈüri÷úœ…k¶º‹Í«æÒ·Þý_"’g0C–ì`ãÌר¬¹otîË’ýxnôwü²r#ÛCö±/d;kN k];]’Õ;ŒcÞŸÛÝ·‡–3ñÅdÜhùÉföüÚ— ÿ &¨ï¯„®}—†šìæK—ÍzVÐrÀ,\¹™]!ÛX=o<k¥Õ+½Þ«6¥Õ{+ÌA§×Þfò‚•lÞÂÎ ¿òE¯à{·{QÙ.ELúq½5kÖtzÙQ¦=éï¡8{HÚø>O½Îÿ±™6çøí¼5áuÚúogŵ¼3háÌ/cøô¯hl(ÄG›uÍbưiΧ| AíòöŒY¸ ìËÜ“¦\Êtd~µûÍŒ®*¶Ï›È÷áF´e¼‰»rïi˜’¶ &L¡£u9ï|¼Š‹)ùY–ª ƒ)ù=ã§œÂä^‘–=‡0bê.¾1‰= ÿ•&uå“w[rcÞ' Ü{¹t<£oc#…÷ÂÔº1 Ë,äÜ H>þ>‰R-û1fð§ô;Ú‰ÿÞ·r\¸]Š®‚¸ paÝYøAv>,Ž–i×±w6¾ÍÁDËÞUlº5‡W_ªÆÊÏ`ÍCQ¦[—8w.*#,$¯§èÑ© gtgÒo°_Æ=h ={´ä×±[‰Ï¡<‡æ÷|’žáÜ‚îLøõ½õMÿŒ©üi5v(Ã+ìfüÛ?r$þþÛ’;^W…Ä ‡Øv+ù':€f ÚÑ¢†š=‡þ ÙÇÉÈþC9~: 81.áÀn[Þ£åã>ü¾æŠ®õkÙ8úÝQL>³/Ûõlxšî¯ú±Ú@æoAÎLó§ÅÊ~ü_ýéìÙ›VïKGØè$VŽ]±OôŠdçªmìKŽBƒç¿$8¸<ò¿Èé«·0·KQ”ÞíP­È’Ð7[XНUÐ ¼Xó<­;…À|œµëÎS¹]{{À®`UåZ<ª‹æßCÿ…ÖK:|·êµ©”ËçבùÕUjSÝ-uû_2å^ý˜ñmL,?]·íGK~ëj»v™k6o| ÷nnËÑU,ó¢Ë·Ëù~\oZÕð!½%&„-­Ôº>¨«5 žþ4ûöß%%‡ù²]O•jäîNË×Jhh(»W¾C°ÆÒeÜíì°³qëÆ-½>ºô ßææmððÊý‹ÂÜ.‚_AkyìŨæV‡a+öFXØ~–¿]]`^j’z¨ Š › Tyþv–po½óKr.Ó(ÄÞÂÞéüA?‚=³›:uU¬,Ȩ²nmsÿ{§#ÇüÂY¿W¿øw~x³:%–û°4nÍS¥ÔTnÚ”r‘¡ì‹¶å<_v$ l·Ù8¡Ý»wOºÒù®LÚjÄ^»Ò’bI…,§½k%…«‚$;² s»]Vk^~:¦°®`–~å´â¬è°¾1í[—åøOƒé‘ñÛî=†²à¤g^j‘ºSÆÃ퉀*í’Œ)< ÷üÓY/†sÖTŽà†åÿ[ªJ4.‹éìI.eóù•ÒBÁ‘ù­—Ïi.Gpà ٴL‘«7x"{Kõdêç¯RÉÎ÷H~ëš#%‘ËûW0cD†/»C7^¡ž&µNÆUl‰kÈ‹íšðDËʜ߾‹‹¶\æËn=_>K¤Ù—G+©¸rá2†K܈w^ËÒÛ¥(J¯„„„\¦Ì»‚(Óž’ÀEþãä٬ώ3õ?œ¾g‡›LÂæpz hÏÓ¾[Y{ç!;"éÛ{žÑ°þ¬«á“þßg½Dx„™ÎmûðƉ߉üñ½³‹ÍÇv±dÅ~è3™±ÉsÙpN¢Zû¼Yõ<˧…Üßÿ«Är×(и5Í+EzÉùcv²tõ›|Óo*ŸI Xü&Ï@ô×¶°3â¿¢-W7òù‡•ø~Î;|Öû$ƒ~:Mæ]€J\ëš ¹Â“¼ÞH&âìu’4þ4zÄÆŒé«9ù«ÖF± û8Ê{ç·ÏSûRsœÏ–ÝzÞÉòÕoòuÏÉ|n]ÀÚ#×1ëËdˆâ¯õÇò\÷û¸p»E‹FéS§hÚ´©SËwjyÙ) å)òܲ}KôÇ`WtÖaãÚέœ2„þ¯ë~»Î™%ã˜àýÞš@ƒŒÉx‹k'Â8sÇ J,Û¾JÃO†0à‹gÐ$\fïÜ“l9ÉñÞatòH÷úŒv¾pçìšÉâvþÍlרðÓRžü Ã_ÝÁ¾Ùǘ?Cß ãÝØá xý¦½­Ãr÷Ûg`wĽÅ'XÀg?6aÑÛãè±»/?ɼË=9ouÍ…¦T5žíÙá h-q\ ßͬ ¿p&£%máìÊå„uKã¿°)­ù›ó|ٯ烳‡2&vý_ÍÔþÍÙ­ß°mƒØ¥Û¥è1™Lh46oÞL“&Mœz,ð¶mÛœVVN ú lE½­"eyžy3 ªLƒФ=jÓžg~ü¦nݺõ æ-JWŸ‘K¿$p^gÞÛxÇn_­P4ùøø Iýû÷§FN)óäÉ“,X°À)eåÄf³[àËÉÎñãÇÃ3’åÑ’öÜX3 ¶Lƒ’eHwÏ¿PÑï\À€êµy´Fc^}ïÚ',gþV¾›ÄÄD–,Y‚Éôà&&“‰¥K—>p9ŽH¯{q'X¸Ÿª2/Œ›ÃâSéYnŸ¿¿ˆÓEû·`‡ÙlÆjµ’œœÌwß}÷@}ª6›9sæJ·€Åb!%¥d|àD„ sé] 2.Çïc6›™3gQQQTÃÿ¸ºë!è‚Á)ŒF#Š¢põêU>ÿüsN:åð¼áááL˜0¡ÐÂ7..®À—S”ù£ Ax06›˜˜ ÉÉÉÌŸ?ŸR¥JѪU+jÕª…»»{ÆéÅ‹…„„ÂÃÃÙ¶m[ß”3Åb)qá "€¡Ä0hµZôz=wîÜaÅŠ®®’¸-½«+ Bá1›Í˜Íf4 :•J…$IN¿fpvEAQ¬V+&“©ÄìlËŽ`A(RRRJ|øb'œ ‚‹ˆApÀ‚ ."“ã IDATXÁED ‚ ¸ˆ`A,‚à""€A\D° ‚‹ˆ3ᡈHJJÂd2aµZKÄ ) ‹$I¨T*t:z½ÞÕÕ¹‡`Ap1«ÕJ\\²,£×ëQ«Õ…vm†’@Q, &“‰ääd *•ýû`6Ñ!.‡F£ÁÓÓF#Â×É$IÊX¿:£Ñèê*e,.””””Ñò ž››*•ªÈÜqY° ¸ÉdÊó-‚„£Óéœr“Rg,.dµZ3îF!µZÅbqu5À‚àRŠ¢ˆ>ßB&IR‘9ÊD° ‚‹ˆApÀ‚ ."ØÙŸg†MæÃ—+9gÉ~<;rßrî —|î8Œþ­Ê‹ )¡âõ+yX¯%Mƒÿ‰"v¢LÕWºòxʃ»Ð©Îf3»ºR€í+ÞïÇŠâX9€àþDoûŒe;Q4Ž͉„¶lÚeî»§0ø´¦ÿ¸)VëWÔ&ÎÖU¸rl+æ|Ã¥\‰žãxá¹·˜?‚9[óPœ¶—½×ÑZGöÂpw$M¿ !ÝøÞó&ÌG–p¤@Gø´ ö=Ïv,\Xÿ:³¿OÄ ]¾€Å¥×½]ô§eÊ*Þ\|½*˹ËNÞOÕ§äœdë\î¿{ý3dÇÙB®í«iÞ{C:ærdÏ&vçkiÑŸÛîíCîÿí -åÅ[ÒÜç—óÀ›Ð–Á(i{¹hRÓ¼ãu“M:š¶k‹Î¶îJà iPr ݾŸ#GŽ$::šsçÎÙ}¼mÛ¶ôêÕ‹µkëô³Y•ÕŸ–[0ô‘Þ\Ù4™˜Ó¾uüšùw¼¿#?®ŠÇ\‰¢ ™É$&¦^'”üoçñ‘í8óÅXüp s8Ÿ°ÕŒ{¼?ß¾¶…k~­¸´¾ßÆnOâc™ÿíÙòõ-ëa¨š3øµ©Lkµ‹¹“Wq4ïÚ_™üÃÑœäy"ºéX·£ˆàðpB òPn‰ ³zG•0z„û’´!†Ë5`!çÔ6ï>‰8œÙŠ>_£_g5û2öÁö/žÄ?·d£ñ‹›ÓïÇg¸«ûûìÝ Ppö{œÄL ÓCéýÅPúuV³÷³ã¯›—°¥ÏŒ;KدÞ­ª­±˜*j//û¯£íÇ•ü˜]2½L¿Þ¬ý) EÁm=Tÿ4½¿“}ßc;´¥PœJbbâÕºHþC\|ßXÈKŠf÷“•zŸVBaò~?ü7þvûí\¸¼™x›AkIÓžî7ù²{=“ŠP€Ì]~´~4’Íw“r!™4em[i9yªEݜ֡*Ò¤bô®xÝäÔ’ý6^IåÜù ;áïÛ·ogèС÷1°‘–³ìÞž„ï]SxõÑ;éI¯ÈŽ•í…9™¸#mîyŠQ·GÒsàpîî¹;Y½ö,7<µ×FÝNýú;<ÙáOþï›Ý׎ÿ*9dé%B#‡Ð·ŸKë+Ù;ønýy:=³ˆ·ž¸‡~="è5p8;iËmº°‰·_ýŠ‹3xë‰ÎvÿX’·ò[|sFŒ„~ç6Í.îÚFÒÍcy¼ûE¶üvÊ¥©RJöÖ¬O¥Ý¸…¼ýô0n‹Œ ç€¡Œ¹¯~5ܵ3žM EêΈ§‡Ñ»kg:uêD¨ï_­°½¼Žv«œ¿ŸŸ~˦ǤiÜaÜÂO{òPÜ´ïJeß7uF!ÿÌþ8oAmue5¥èOŽÅé Â]=:ѦE Z¶íD·ÎÍñ.{ZñyâŠhÙ³íÍIœN.šqiÝZf2™Ø¸q#7n¬7—“¬®z1Ñÿ¾þèb?cgºí_V wláä”) »«~H#~õëÌ|‰g'Ìçîƒ>“‹'Å J[W,"bÞž}ï¼òSØ·ò$¿O"ö³¼P4“çÇ¿ÅÐFpåÌ^¾šõ!_Ÿ°3Âr‘ŸÇ€WF0íÁíü±ì¸ ëçshy/æLãÙ‡_añd ¦¬³lû(š] å‹/<ño­êÉW“_çñ]Oóy|±ÍöSùõçC<{Sc¶l9S2Fz~¿Ç=Ϫ_øßW»^yÄ,›Êìœ(&Þÿ‹&úB^:g¶,gëÆã5 æÓ«™÷~0³ÇÏâƒ1~¨Š Ð_>GljnéÇÿ Ú+þ’ý×ñ„½-8ºþ'™ˆú—‰¹úENöÝ¥1ˆ¢Ê½oê’’Ç™½¸¡E?«^•‘ üÂ&C"o¼»#½ÁË•¤ý$ÅCÉaé§â¸Ü5ùôIRM•YW¨.{Ë$›ûÖ‹lµ¨¬5%ǮՀwé}ëÛå]»ví^3» ׯÌÌL7n\×Õð8W®\!8øÚc ebccQ”ü­)¶¹5•Þ7f«Åbµ(6K™rúëÅ„ ‚',‚PGD ‚ ÔÀ‚ uD° B,u¨>}C¯§¨OßD-XêJ¥j0'\/L&êJ_Πfˆ„:¤Ñh0êÙI \QQM±–KD BÒét˜ÍfŠŠŠœ?Y¨¶¢¢", Z­Öù“kAýè‡ ‚ @¯×c2™Ðh4¨Õêz3FÙ(Š‚Édº¾u]¥«D BS©T4jÔˆ¢¢"ŠŠŠ0™LâÀœI’„Z­F£ÑÔ›žoÀ‚POhµÚzBÍcÀ‚ uD° B,‚PGD ‚ ÔÀ‚ uD° B,‚PGD ‚ ÔÀ‚ uDœ 'H«Õpõª`µuí EQPƒÁ@nn®Ç_„H° x___5j„,×͇_I’$ N‡N§Ãb±••E~~~Ô§®‰Š——Íš5ãÑG%22ÿ«(7™Lèõzbbbøþûï¹téRÖI–eš4i‚¿¿?iii5º­úH° 4pjµšæÍ›#Ë2~~~¼üòËÜzë­v‡Ôj57æî»ïfÈ!:tˆE‹ÕxÕÛÛ›V­Z‘––æQß" BW¾aaaüë_ÿ"""Â¥1_I’èÑ£«W¯¦cÇŽ5^OY–iÞ¼yo§>, XhhèÕðýðët¹KFÃÒ¥K «–'Ë2¡¡¡5¾úBpuÉ! š¹”wF…U½1¥ ‰bâà–žù‚HAt1™§µp¼ÿrsîˆZÈ«kSõ6’›ÒsÌÆõkâíìëë‹——¾¾¾,^¼•JUå²T*‹/Æ×××5´ÏËË ŸßN}à ïÃ@Xd/n ÑàôCäK‹[úÓ+̯üs¥`"GŒæž›—ÑIÁô|d,Cor¼ÿR#n0î-tUo#¹ý}”ÁýªZÂu¥qãÆ(ŠÂ«¯¾ê– ½kµZfÏž]+ßÖѸqãßF}ÐðÂIÜ5ÿ{æ F#+˜d]LâdôVÖÿßìJ.¬½º¨»0î½Etþv41‰yˆ/jŠN§C’$BBBw[¹‘‘‘„„„‘‘á¶2í‘e­VÛàç {@X…P ªãŸ3íÙI WA_ö»SQ™×TÛ…íßFË tr©\ýåŠÛ–’ýê;•Ïuæ¦P/ò’cØðÙRVnOÅèèEuVMîšü‡EÐÚ§ˆ ±g0H\ó’4@ÞÞÞôèÑÃíe÷ìÙÓíeÚSv–^Cæ¹?Ó6®\Ë¿~b8»…•oÍbÚËKÙZܗ鋦Ó×@Gø”x÷{?™Í³Ï½Åú^ÌZ2¾~tˆ§ñ™¯x}Úd¢Þø’ÄvòbTor7­`άÙ,ú­ˆÞÏ¿É3ݽJ·æ¬Lk.n?éæÎœÊ´×—±ÝÜϪþÎÚ@Bã•Ãþ¯ßaö¬·øW\3^øS{8:ã¤N’}f|Ìüšpìó9̘9Ÿ¯bòÐzyÎ(»¢(¸½ÜÀÀÀ«§פº:[¯6yLØKÊiNç©èÛ¶52éä%ìcGécq§s ò5ûµCµ5–’©áò’¢Ù}à$æ²BÊÞ#ªæ ~m*ÓZíbîäUÍsüæ4d&“˜˜ŠUç°sj›w—”}8³}¾F¿Îjö% dìƒ!ì_<‰nÉFâ7§ßÏpW÷÷Ù»@¡ ù(ûÄÌQÒ[æ¶ñIìX·•?Šcpë½Ko‰|ä,J€“2÷üÕ=·lûg±÷ÀIÌÄp0=”Þ_ ¥_g5{™\h[3é;Wóåú’ý߈wûïxrd?Vü<›6tV§}'2rXSN¬œÊ»ÿw¾¤­ë¹áþ~twñýq=“$ EQ®žáæN5Q¦§-y•7-N jü`ºµ FkÔcôQc>æÊÇ ™fÎa®&ÕÞgçe‹óUœ°\L!ÍHP€ŒªMgÂ||h1g»ßøk›j/ EÁ>vfXȼ”‰¢ "H¦Ëd\†ÿ’^§ì´LãÕaˆÊo¿¤þKë_¥¶µ¤rìx&š^h¥úS6;«“ºõ ´÷Jgû±4ªÿjÖjcü×SxtËmnâF?3)çR l,ï.ù¿ðæ’8®(mxèÍ t©$…ìÿsºÃPF¿ò LýŒ#ô€]*ÑbÆ„ŒJ$ ,—Ù4?НN™­že¡ SBð5뛊M ée P@)¦Ø¬ ÉRI`:-ÓJ¶¯˜MWë/W±m%YÅb¶ˆ³:µS©Ö®òUß”…¤Édr{µ¶Nö„ ÷ÜöjÅ}“GÒ1o/omÍÀ«G:HGxoÕèóIÑ»ÚwR0$­ãõ%™÷Ù\½ÆÄÙëH¶}Ÿ*EŠÀ7À·RsYÍ)gH2Žæ†6*ÎoJ¼ö R†Êœ–éÆík:V¡m½o wD ÎÄ‘jJÏ!dÉ¥:Içâ8cMÏÞíñ:~Æ#¼YSI’ÐëõnŸS«×ëÝZž#"€9¨·öèÑË—Æmn¦ÿð‡Òî2ëç,æ×L ÒÙR¤ÑŒxz›È25#Ô·r½'Ó…M¼ýj>ýû Þzâ$Ï}~ƒõÌÉÄ%}ÏSŒ:ño¤æ4º²“Í'*.WÉÞÁšõOòñ¸…¼mþ‚Ÿ¦aÔ5#, •_þwüšñQW8-S©ÄslËèRÛJø¶‹ o¤†BMKz>4‘±-Nóå›ûÈPrÈÒK„F¡o›Tö$;©SîVÿð'Ÿ>¹ˆ…üƒu‡Ó0úÜBÏè ´Z-àž{îqkÙÑÑÑn-Ï‘†><"€Íäåä`éõ~úc>YirbÿWÌ~ÃêDŒÓ«™÷~0³ÇÏâƒ1~¨Š Ð_>Gljn¥N˜(<ño­êÉW“_çñ]Oóy¼UßKÉaëŠED̛³ïÝW~ ûVžäw' yÄ,›Êìœ(&Þÿ‹&úB^:g¶,gëÆª°Ó2•J<×É–ÌNÛVOÒÁ£\ø —àeÖs>v;Ë£>aíéÒƒ–‹lüü;¼2‚inçeÇÔß@ìÊ(¦åL幇g±ø™dƒžK)‡Ù~®áŸ“››‹V«å»ï¾ãî»ïvÛPŒ¢(¬Y³Æ-e9“—WµwöõÄÞ«"ÙÜ·^d«Eeµ¨¯Ò[ïÒûÖ·Ë»víê Ÿ¡ÞhÑ¢’$±`Á·ÍŽŽŽfîܹW/¬^Scì‹…ÔÔÔ)Û±±±G(ÀHÉalë[Sé}`¶Z,V‹b³”)÷·¿áO´• ÀÂ… Ýòq¾°°wß} Fà ++«ÆÊ®OD B•ŸŸOqq1ùùù¼ð ˜Ífç+9`6›™5k………5>³Äh4zÌW‰„J’$ÒÓÓQ…„„¦NZ¥žpQQÓ¦M#))©V†<髉D BfÂIII<öØc8pÀ¥)^Š¢͘1cHHH¨ñaO _ðˆY‚àÙL&.\ Y³fäååñúë¯ÂرcéÝ»7þþþW¿¬³¸¸½^Ïþýûùî»ï¸téÒÕà-»6CM„°Ñhô¸ðÀ‚Ðà•]"==NG£FÈÈÈàÃ?tض]랯»ÃW|-½  ^YpRXXˆV«Å××ooo»C ÖëÎð-»ŠšÑh$''ƒÁà|¥L° x²-**r逜§^K£¶ˆT¬ A5O° x0´uKLCA¨#"€Aêˆ`A„:"X¡ŽˆA¨#"€Aêˆ`A„:"æ ‚êåkfD##7é,he¯Zš\¬@‘q…jþ“åELžªv6\O‰2$ÐÄ”þªÚùV<Û­¨%ð“ —¯‰^¾&r-+Ò¼ù]ïU+õ©oD ‚ðÆÂ'íŠè µÔø¶@Ò߇§¢‰„ìêÒ€5cÉÍÂphyÿY†_Æ^ma`d“b¢þÔbô°QQÀ‚ÐÀ5S[XÕ¾¿îõ*€ìDÐŒexßÒìæ¬öBn‚nðhtwŽÂxt9F†ž:0)ɇt“ç„°çì© x ¤ÔZøªÚßLðg{ñî>À~øÚ’$¼Ão'xÕ¨;tÃO†• ð¦æ{éõ…`AhÀV´­Åð}o=’FWéõ%Ž&ï®CÕþfüdXÑ®úßà|½\RÝGLæéA-7 Üœ;¢òêßÚˆFêÄà@íkxÌW$ß š¼½äjÌhU4™ÿ’.€Z ƒŠÝVÇúLdCUHÁô|d,CoÂá-©7H÷:ÇϹ^H¾´¸¥?½ÂüêϾ¨»ñü÷›Y;³šº®K=b¨Ñ×Kg.¯RÏ×–¤õ!pæÇ( Liæß”Ñ`ÂX¾‡×úxÛüÔĉåc˜ðõ9eruƽ·ˆÎߎ&&1ïš©EuÂR@Fò9Î¥çŠ×ÒŽž¾æZ™j&‡´BsËmn+O>¹i 2/ék&&¿aÏn° P|øÌü{4ý-µPp>MüÂ6–DÖ¾ü këºõÔÃ5Z¾X|$ʵn®’$üŽ"ç³WѸXðõÌ¢?Çñ#G(°}@áΙó˜p[-C‚ÐQÈ¥3»øþƒÅ¬‰ÍCA¦ÍCïñÁ¤^´Ô`Ê>KÌ—óÞª½d\Mo™ ¾SùlPgn õ"/9† Ÿ-eåöT¾õÕ¡ôŸ0§ïíAXˆŠ¬¸í¬^º”ÿÄÙô*%_:=8ƒÙO ¦Ks-ÅYÉìþd&s¹XòÇÃY9r"ŸÁ”GúsC…—âX3/ŠÏcýé÷ìË<=¸Z¡1f²cÉ^ßÅiÝÔt›±Žý3 l}í.^Þ\äž:ËMìÔëy6öøœ÷:ÿÀøqÿ$Á ÑôáOX÷üÞxà+:®üŠ{÷=Ëè'1W´ßÇŒ®·}q£Î\ãÛPPÐÜz‡ÛËÕD BAá&­Éíe×7 :€  CD8“>eî»§0ø´¦ÿ¸)L_4s£°7_áʱu¬˜ó —r%BzŽã…çÞbzüæl-û…•Ðxå°ÿëwø"]¦Ý=˜´ð#¼¢Æ³ôà5‘èŸòïÞÆ×Kg³(=ÞOÍfÖ’é\½€½y=S{”y/öçÒ?æ1iß%ä&íðK¿\ÚswVކymA‡IDAT.?fé£*¶ýã>ÓãHîyЈ.úÓ2eo.>‚^ˆåÜe,ÎÊ4˜ˆÿf6oþ’Ž…¼ôòctÕªs½z'ÑpÃÝ‘D4ý‚„t àKxÏ›0Y‘BèX®í·ëmßPèjx°^QJ•#·—-4FQ@[o8ÔœÀšï°ãÀ;Wÿo¹²ŽéÃò‡@¡àì!ö8‰™¦‡Òû‹¡ôë¬fï!y ûØQº^Üé\†|ÍðníPm¥äï²™ô«ùr}Iïk_t"Þí¿ãÉ‘ýXuðwl§¥€Œ}0„ý‹'ñÏ-Ù(@üâæôûñîêþ>{÷üÕo–ƒ$éÙ(†ØÓ…Ài×Ë9:€q£Û“øÅXæ{–rý ÀB^R4»œ¼ú˜Ó2£KžgÈL&11ÕîNµê¼ÏA½ wqÈô2ýz²ö§,]·õPqüÓhôJ`ù ø9ÞïÊ´}C¡®ÁðR¬–«g¸¹‘¤öB¡f÷¡¾hÐlŒù„ÉîåêesçM`ïаåb -È€7-N jü`ºµ FkÔcôQc>VÁñvK*ÇŽg¢éÕ‰Vªß9eó°ªMgÂ||h1g»ß(û©ŒÚËBQ°Æ«…MÇÖñõAL_±†›~ý‘ÿ{=ÛOgcv¡¯v]è¤Mgç¡T\ýê¼nÎU§ÎŽÊWrö°%ÆÌìÛûøóÿ(ºu½½°jg&ʰº‚ýv¶}]·ì¾ÍÝ»»QJzØ5U|=Ò XÉ¿HB|üµcÀvÞ™ŠÙ„ • rØXÞ]0ó?àÍ%q\QÚðЛ èd{’,ƒb±ÿÆ‘$°\fÓü(¾:e 2õå×1&ð3ao¯ûùØ£Ìýzc>bêWq˜•Ó^®ü/^eêæHuêL°ý2•ölŽæÅWs[Ðv²îìöà'츬\ûJì·“í7Df¥vzŠ©Éͽ`ÅTò³á7ð®*MÇ.tŽðÞª Ð+ ç“¢w2wÂûzG4¡àL©f ôà­$—ü˜SÎdÍ mTœß”ˆÓiæJ)ûײtÿ¶ÌøŠOG=À-߯ã¤)%ž$ãHÂ#Z¡Š=ëR/ØiÝÔEŠÀ7À·âp¯bÏFWÈÙ½†ù ½ï>²¨Ù»x;Yvºwæ ö»Òmß*à_ lÉÍBÕ(Ľeê¯Phiøc :€åÀö„÷èõá"KÁNv¸ Ƴ ¤H£ñô026'ejF¨ï5].|ÛEÐ7RC¡¦%=šÈاùòÍ}ä(9dé%B#‡Ð·M*{’w°fý“|i¦ì ¼¸Çþ¬¥¢ýŽw½íЏB5½|k¾Yxh~ƒG»·Ìƒ[8YØðÏkÐìþ4öt¹Ÿ™âW2îÉí®g>½šyï3{ü,>㇪¸ýåsĦæ–þòëI:x”+Ÿ`á²¼ÌzÎÇngyÔ'¬=]:žh¹ÈÆÏ¿cÀ+#˜öàvþXvœ˜eS™ÅÄû_`ÑD_ÈKçÌ–ålÝX>¼wdиǘÖ*oS.çãvñÑüoˆ7ä9)'ŸCË£x1gÏ>ü ‹'k0eeÛGÑìrÀNË´ä°uÅ""æMáÙ÷îÀ+?…}+Oò»UW«Î¾Ž®ÿ‰„G&¢þåGb^& ‚ýŽ¿ärÛ7ÿÎò. àš –$ÈZ»¿;G¹o.°¢µv9’ÿÍjøç8Úk5Éæ¾õ"[-*«E x•Þz—Þ·¾]Þµk×î5³ ‚ ØóßNyø×P'Ò¢€YQ(¶@ó¹ÿB{«³#$®)<¸•ôw&P¨HŒJôsK™U{ˆ¢äm±Í­©ô¾ 0[-«E±YÊ”ûsßðûø‚à¡–]ÔÖØLI*Yd ..™ŠRdoî{å(Eù¤}…,ÁßÓm/#Ð0‰„jk®šDCÍýŠKH¨$ @Oêk#ÁR³ïÌfR_} óH2ÈlËõŒ¯(, ØÔ?µèÍî–(éýJ¨% cR,çfÝ_¥ž°b( ù¥)>{’B‹Ä¬sZ$w^_¢, X12Ïý©#·†®@%#!Kà%K˜Îž qBo nwí, E¡ààVŸìIqâ1 ,Qç|(–öx¬5èY‚ @ºIfT¼ËÛÑÑÛRz˜½ú³#¤ÒJNQ@–(.Г2ÿ TM[Òtô4|#!û7¾z²†b*Æ¢¿B~Ì2~XŽ9ã* ÎU¼˜\ò¥œÒùD ‚G0"3éOn÷71£Yn:M®l(‚Ò–d “¢`ÎĶ5«ø6:K]WÐCI>í¸}ÔcŒÔƒN- 2øóØ.~þö;6Ê)y]¤FÜ¿ø¢ ‹xàß)À‹VÃæòÑô0¾;“EÛÓÅkˆ`¡’üºóÌ y´å%öüò-žH£Ð»1ínŽ @k¢~L òË—3^du|áÕGvoù¹ôžL뿽ł§zà)'™ÃV²ôËýdZ¹)·Oy…ñ}ÚÓ¼i : ÉLØÇÚóŸ“y%.7æÖÑÏ3ñ>tl¢¢0#žÿ,x‘¯Oƒº}ŸÌ¸!·Ò>XEvü.Ö¬ø„ŸNçypøëònϪé/±&±ìL²=ìøm§æÿ3žcïáì̵^OMèà—YòB7—ÍfÁ¦ó×Aíë+ÀBý¢`ØP²·.c­Uø–§û3«ÞYCFžDÓˆ1Lð“ÏŒåy(øÓ¾û-4úóŸ,\AÛ’¾>Ëä·&“üÄûDh¸ñ‰E,©b×Wïóùé\¼›{Ñ èè6qóîLgÍŠ7ø(#€Èǧ3õɤ=ù>ûëÇ%jŸöV—·|ÀºD›Óx‹Ï³ñ_ÿcä'ro¿výZö€-ï~•ù3»rfÙ‹¼³1E„¯ ÀB½"·¡¯…¤Sgp|¶¾B^R4»KÿwúÌ?i7hConƒjçIL¥ÏÉO>Êþƒ§0s˜#͉üd}nPØ—1·ãìê§YôC2f«’%ÿÛ5<˜˜¦ó¯9(@ÂGÍèýíÜÑí#ö¨±]¯×äà6´ö)y]Œv7ýG¢qamC‘I@Ým"KúøpþóçEø: X¨_$ P”ŠF ½hÑ<Ï>:›[7F[œ‹Q§Âëøz –´ó¤)H¨ÛÜHGÍ%ö½P.|T­o ƒÎ‡æ/ý›ß^ºZ)TjCžùPÙ}·d':½ƒÇLg|ü›|y0KŒýÚ,Ô+–ÌT.ÉtíØ/ÛímÉF3oÎ,VðÞ²x²”– e.ý+(W±˜0#£’Iv&’–+ü¾è%¾·¾`‹Bá•\šWu×®k–Ë©¤Êtï|Þ^óéDݾ3¼‹9Ÿ|KéìVKÚ.>šÿO¼¸€ßù¿·g³|ïeÂVÄ<`¡~):Ì–ÝÙyŒám¼ì>EÓ¾3íåã¬ÿòWžN")!žó¹®3§&ðgq·toêšÇ9[DX+™‹ÉÉ$_]RÈÈû+:êË7*ÔšÂCüº5ƒ&wåoím>i¨[pϸûh•÷¿íÕ—;P©SØüÞ æm‚¡só|d GаeûþƒòŸ4¤ ÙfQYÝÚ.ÃBBB<³ëЀäæRœ?u‰VƒG1úo½h®VPùѬÍDºŸAMS8x® ‡ßAÍe.åÉø4jEø A´KßÌö§a‘~ÿp:œßÈúƒ™%àÆàÑý°ìüŽíñç¹41#ÑFÉÅ   yÇ[ óN%ùb*Y2ò¡´—ò)RùѬÃ-ôë¢#9)ƒb¼ <‚¡…$;Åy½{G6srrðññ)÷³šiçÊ*æb\2þýG2î¡Ûhé­ ëÓºëí<2õEƇØþþ;|s*EÒÑyÈHz›÷°fk&¥Ô˜ý¤ux€gÆt'wï6NåÔm?Ø^;[»téR:°0;·e‹â`q‰‚êKævÞ}>“ØÇã¾á“¹3ØUqçNðÇÞ( kXøq0ӛʇ}‘‹ Ƚ’LÜW§‰ptål^Ïyާþ6“·ŸÖ`ÊNfç§Ù›ÁáÏ^äõœI<94Š·žôüK$m_ÉÎÍoIcó×ké7ó~ž¾›Ÿ ~\Y¶æ)9Ñ|2m '}œw=ëãü‹.söØ6–Íø†_Nd;^0§±eÉB:-_³¯Žáø´oHGåì~¨¨lÛã-[Ô€Wé­wé}ëÛå]»ví^3» Ô•ÌÌLÚ¶m[×ÕhPÎ;Gppp¹Ÿ‰vv?{íl-66ö(b›[Sé}%=â²¥¢žq™r}1,‚PGj"€›[A„ëYešè ‚ Ôw°èñ ‚à Ü–uÕ àŠ*ápÐYá:ájŽU9ãªÛv¶a¾ ˜$IX,â¼&w±X,vOðíì^ŽÚÙ͸ª°«½àN¥Ra4Ú;YX¨ £ÑˆZ}íÔ|ÑÎîå¨mT%¿*s• `{°žçf;ï­Rg…×Fƒ^¯¯ëj4z½FsÍÏE;»—£v¶£*ÙV©¼«ê„+ÓÑ8N‡Éd"''§®«rÝËÉÉÁd2¡Õj¯yL´³ûTÔÎv8ʰjowŸŠ\­ó¢…ëO@@z½ƒÁ@@@ÞÞÞȲ˜Ýè ‹Å‚ÑhD¯×c2™pø\ÑÎUW™v¶QãyVvÔ!ìA$I"00¢¢"®\¹‚ÉdBQÄËí I’P«Õh4|}}>W´sÕT¦­T”cnfu%€Ë wtØÐÞ`uY¥,ˆðõZ­ÖÕuB5ˆv®UÖfo8ÂÙÔ4§ÙW™Ï0®lÌÞ"æÏ‚p=ªèr“n™\ÕihΆÄ„ ׳Êf[•rÎ^WvΛ£aëK³ ‚ \ol//éh8¢Êó„«;ØQOØ6„EX„ë‰m~9ºÆ¯+SÔrÀŽæ»¹²”UÞö‹gA®ÖY¯êPk…쎉„öB×v=`A®'ÎòÌ-Ƕª:ØÕÞoYX‰Áþ—{bsŸ î ‚ T†£ic®L&põ 8«ÆŽÂÍ^Úûn8 Ç߆¬vrk½X«²dukÒÎê-‚ç²¶ŽŽMY˱õbrrkûpfÊw:+#¾&¤«Û–lvÎ:4ÍV·P>4mE.}žu°‹¡2œ°£aÛ`µ²Ž†! =`wœŠlo­CØÑ:e;"Û, S`ç¾ ¸ÀöBØQoØ•o;®ÖX°«§"KV÷ËX÷~mÃ×:„í•§P¶e·Ž†*A°ÇÞX¯£¡Û¶ âŠæ;G¶W»°uè:úYE]|ì¬_ö\¯uØÙد_A\e;[Q;ê Û~p¥l/xí†qU† õ‚+:ã­ì9ÖákoØÁú>vnmï ‚ Xs4 QvkÛ‹­(„ýßÑp¥‡"\ `ënözÁe=Y{Alû—Â6x ;8êùŠÁG½ÑŠŽY9 âŠæÛ†°½í9U݃peʹ,ˆ…pÙó0vnqðA„2α¢¶ bGÿ¯vÏ·Lu† ¬Ì•*”aÛnÖê*Óó¡+Be9ލ¨'l/Œ+z6åVJElo¸ÁÞðƒ½Ö![VQë! Û™¢÷+‚»T¶\QÛû¿³àuå Pù°m::Úg¾ÖáªàZ¯Wô~ApWʹ²8z°W¦Ëª;ì(„Áq×Þ©ÍØ¹µ½/‚P®†°íÿí-ŽÖ©WÎÞsì…¤½pµ¾_Ýàa,‚«}:·½ïJã侫ۼFUØöçŽÂ´¢P¶wëêöA*â(øœ…pÙmEaëjøVôsÀõP«L[߯làŠðÁ]*ÂÖ÷+¸U _¨\°¹Ž®†¬«3Dø ‚PU®†£«!ëê —Æ…+n=¿¢@­¨—[™2A*«¢0¬(P+êåV¦L‡ªp• LÑÓ¡¾©lÏØ•u=vª†ž»ÞU§|A„Êp%«3¤PééhÕ :wŒW§LA„ªp5(+¨Uš ì®À«­ Ap§*ŸDQÍuš ?¨‚ 4TÕ]kµ–"A¸^¹5pmÉΟ"‚ ‚ ‚ ‚ ‚ ‚ Beü?ÜP\™Ò‰rIEND®B`‚OTPClient-3.2.1/flatpak/000077500000000000000000000000001452112020400147275ustar00rootroot00000000000000OTPClient-3.2.1/flatpak/build-flatpak.sh000077500000000000000000000004001452112020400177770ustar00rootroot00000000000000#!/bin/bash flatpak-builder \ --force-clean \ --ccache \ --require-changes \ --repo=repo \ --arch="$(flatpak --default-arch)" \ --subject="build of com.github.paolostivanin.OTPClient, $(date)" \ build com.github.paolostivanin.OTPClient.yaml OTPClient-3.2.1/flatpak/com.github.paolostivanin.OTPClient.yaml000066400000000000000000000046351452112020400243470ustar00rootroot00000000000000app-id: com.github.paolostivanin.OTPClient runtime: org.gnome.Platform runtime-version: '44' sdk: org.gnome.Sdk command: otpclient finish-args: - "--share=ipc" - "--socket=x11" - "--socket=wayland" - "--device=all" - "--talk-name=org.freedesktop.secrets" modules: - name: jansson cleanup: - "/include" - "/bin" - "/share" - "/lib/pkgconfig" - "/lib/*.la" sources: - type: archive url: https://github.com/akheron/jansson/releases/download/v2.14/jansson-2.14.tar.gz sha256: 5798d010e41cf8d76b66236cfb2f2543c8d082181d16bc3085ab49538d4b9929 - shared-modules/libsecret/libsecret.json - name: protobuf cleanup: - "/include" - "/bin" - "/share" - "/lib/pkgconfig" - "/lib/*.la" sources: - type: archive url: https://github.com/protocolbuffers/protobuf/archive/refs/tags/v21.12.tar.gz sha256: 22fdaf641b31655d4b2297f9981fa5203b2866f8332d3c6333f6b0107bb320de - name: protobuf-c cleanup: - "/include" - "/bin" - "/share" - "/lib/pkgconfig" - "/lib/*.la" sources: - type: archive url: https://github.com/protobuf-c/protobuf-c/archive/refs/tags/v1.4.1.tar.gz sha256: 99be336cdb15dfc5827efe34e5ac9aaa962e2485db547dd254d2a122a7d23102 - name: qrencode buildsystem: cmake-ninja config-opts: - "-DCMAKE_BUILD_TYPE=Release" - "-DBUILD_SHARED_LIBS=TRUE" sources: - type: archive url: https://github.com/fukuchi/libqrencode/archive/refs/tags/v4.1.1.tar.gz sha256: 5385bc1b8c2f20f3b91d258bf8ccc8cf62023935df2d2676b5b67049f31a049c - name: zbar config-opts: - "--without-qt" - "--without-qt5" - "--without-gtk" - "--without-xv" - "--without-imagemagick" - "--without-dbus" - "--with-python=python3" - "--disable-doc" - "--enable-codes=qrcode" sources: - type: archive url: https://www.linuxtv.org/downloads/zbar/zbar-0.23.90.tar.gz sha256: ff857dd7e3dbe043dac3765b5182c91dfd0477800713a75d15287d797cee60fa - name: libcotp buildsystem: cmake-ninja config-opts: - "-DCMAKE_BUILD_TYPE=Release" - "-DCMAKE_INSTALL_LIBDIR=lib" cleanup: - "/include" sources: - type: archive url: https://github.com/paolostivanin/libcotp/archive/v2.0.1.tar.gz sha256: b111d528bbde7c1a0a392f49293b25ae33e6e78fbcbe378e0cf8bc6d59743d11 - name: OTPClient buildsystem: cmake-ninja config-opts: - "-DCMAKE_BUILD_TYPE=Release" - "-DUSE_FLATPAK_APP_FOLDER=ON" sources: - type: git url: https://github.com/paolostivanin/OTPClient.git branch: master OTPClient-3.2.1/flatpak/run-flatpak.sh000077500000000000000000000001331452112020400175070ustar00rootroot00000000000000#!/bin/bash flatpak-builder --run build com.github.paolostivanin.OTPClient.yaml otpclient OTPClient-3.2.1/man/000077500000000000000000000000001452112020400140605ustar00rootroot00000000000000OTPClient-3.2.1/man/otpclient-cli.1.gz000066400000000000000000000014031452112020400173250ustar00rootroot00000000000000‹~àú^otpclient-cli.1…TMOã0½çWŒz‰¸Pnhµ”²í HÔ„•V”Ã4uZ/‰ÙNKÿýŽíдժRcß¼?Ï ›õ ç°µªF+ ¬ª-,¹ä-_À| öÃj”ËÇ lST‚KÓ® 7¸„Ÿ­ä0¸\ö wtØ5»"k¡j"XÄ• d©4$y:ô°^IJ1<ß>¢Yy·3ÇÃÇ ÌÊ4†Ó®ÎÁ{g¿Ÿ“4›d“eÄJ¬aèÎŽR!.x•“$Í'ÉsFÛ·(`súˆ£%QÞ²átâ±ÑqR€uS‰R:k®PT¹Ï‹E,M£D’Œv£@5NI@ÍÁ´M£4){±I ³³y ×Q%Œ½»"ö¨*h´ô&n…BÇE¡Zg” J´\­'’Öhqކ³Cj³R›Cj·÷hJØ Š°k.?ô9ó:trEc^54þ”ø”´ÎÓˆÝ9ã©yn¯G»( A(¦³°C´ÇÄ.‹Pb‚MG^¼'¤ ·Þ‡ëäß5Z-5=dgßÅuÑÁªw¾?qÂGô÷?±÷ÉyžSÉá>¹ŽÃ%—“ÜŸ”kg®¦Ñ*½=?GìÝCØOïîŃsÐ «cßzïK\¬bþ…­¶ïÁÃíA¹â% Ê t`:¸}_ioêâŸ-÷¡% “]D¬ŒòîÎb_€T6$àŠVkê2ÜëÖX‰E@¹Í©VÏCE¾d·?Fÿlïoµ«šÀñ^îÞvËÁ̘û…™“ä£,z vE‰hê!ŠTûNõÏni jA7¸ÃùQ·®¬mÌM¿¿vÕΨ~ƒªRÆŠ5J!û»YÐ߈÷0Qn_òq2ý2L6HM©…µô&4hSGYÇæHª¥EWžKþÕçA£,„)üU†¨ŠdŸ¶kZyýœÒ÷|.Pºùà t÷ø[˜shMô^CBÿø$ЉOTPClient-3.2.1/man/otpclient.1.gz000066400000000000000000000017011452112020400165610ustar00rootroot00000000000000‹~àú^otpclient.1UËrÚHÝë+n±‚Š%™ÌÂU³À€ ãØ¨23©EKºB¤nUw ™¿ŸÛØãIRÙ@?î9}ŸGÞ¶!>`•‘3lûQï uéäÑe>½·Ì4c¹âJS#AÂwœÂéGhjD¿5èñ¸ˆóJó¾KÞ‰JT\&g"¢h˜¨÷4ÆRPœ?  –Óñ•ý|ø½ÁÒòÃx,ßIÅMVè7a¶jA*x,|i³*±:–vJ¦çšÌ'w bñ¾*ß‚Yðé ZæŒ »± ÿ)Šq« rñ2ô÷x„O›•„&¤ÈƒŸ"›âŽŸiÚ³ošzõ¿% $Ì0Û¯¶÷Ï!QM¨’ÓE@‰wïfÜ—à{r •øÐÙû7÷óÛ ÔT™®RMëŒöÀršš—å˜ãó“)Ï'Pf–¶oá™,1­¬´®jV–˜ ¬v$\ï=ø;³X;tc9T%9tÕœ%ÝÔsÆÎÍ §¾[.èQ/ÒA…TÇDUšÒ+¤¥”Ÿ“ŒÞ5 ^£'q–˜—°.›®Ûô„¾ãÝXUr3ÒŸ+°+7#;Ú9A&k°ås΀ÆÌ¥G;SZÿÈ|Gáæ'1êpwÓóá÷°f†XÍ[w¦›E£;—<ÿ§K &;ÕóUàœ~vþÓ¡-P¥±MÛã:\Î-Í ‰å—‹¶·-»­²¡˜â,×W$øBÎ2cJ}=’,eUäŲ–LæR~`Tça§Ãšï[}Ÿ~ —ëÍ…vÖLCMšA `+ê[‰–¡Ú0£Ö¡¯eEcQ²¾ÆÜ*&b®c ñ¼` fL%RKØTΔl¿ÔqsŒ8P*ù cCXâ=B„6MG5IÐ4ÿ\iv£ÆOTPClient-3.2.1/po/000077500000000000000000000000001452112020400137235ustar00rootroot00000000000000OTPClient-3.2.1/po/generate_pot.txt000066400000000000000000000003151452112020400171370ustar00rootroot00000000000000Note for myself: $ xgettext -k_ -kN_ -o otpclient-cli.pot -L C -cTranslators ../src/cli/main.c ../src/cli/help.c ../src/cli/get-data.c $ xgettext -k_ -kN_ -o otpclient.pot -L Glade ../src/ui/otpclient.ui OTPClient-3.2.1/po/otpclient-cli.pot000066400000000000000000000136371452112020400172270ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-12-09 14:00+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../src/cli/main.c:72 msgid "Type the DB decryption password: " msgstr "" #: ../src/cli/main.c:84 msgid "Error while loading the database:" msgstr "" #. Translators: please do not translate '%s --help-show' #: ../src/cli/main.c:103 #, c-format msgid "" "Wrong argument(s). Please type '%s --help-show' to see the available " "options.\n" msgstr "" #. Translators: please do not translate 'account' #: ../src/cli/main.c:120 msgid "" "[ERROR]: The account option (-a) must be specified and can not be empty." msgstr "" #. Translators: please do not translate '%s --help-export' #: ../src/cli/main.c:130 #, c-format msgid "" "Wrong argument(s). Please type '%s --help-export' to see the available " "options.\n" msgstr "" #: ../src/cli/main.c:141 #, c-format msgid "" "%s is not a directory or the folder doesn't exist. The output will be saved " "into the HOME directory.\n" msgstr "" #: ../src/cli/main.c:147 msgid "" "Incorrect parameters used for setting the output folder. Therefore, the " "exported file will be saved into the HOME directory." msgstr "" #: ../src/cli/main.c:157 msgid "Type the export encryption password: " msgstr "" #: ../src/cli/main.c:175 #, c-format msgid "An error occurred while exporting the data: %s\n" msgstr "" #: ../src/cli/main.c:178 #, c-format msgid "Data successfully exported to: %s\n" msgstr "" #: ../src/cli/main.c:224 msgid "Type the absolute path to the database: " msgstr "" #: ../src/cli/main.c:227 msgid "Couldn't get db path from stdin" msgstr "" #: ../src/cli/main.c:234 #, c-format msgid "File '%s' does not exist\n" msgstr "" #: ../src/cli/main.c:256 msgid "Couldn't get termios info" msgstr "" #: ../src/cli/main.c:263 msgid "Couldn't turn echoing off" msgstr "" #: ../src/cli/main.c:268 msgid "Couldn't read password from stdin" msgstr "" #: ../src/cli/help.c:38 #, c-format msgid "" "Usage:\n" " %s
[option 1] [option 2] ..." msgstr "" #. Translators: please do not translate 'help' #: ../src/cli/help.c:44 msgid "help command options:" msgstr "" #. Translators: please do not translate '-h, --help' #: ../src/cli/help.c:47 msgid " -h, --help\t\tShow this help" msgstr "" #. Translators: please do not translate '--help-show' #: ../src/cli/help.c:50 msgid " --help-show\t\tShow options" msgstr "" #. Translators: please do not translate '--help-export' #: ../src/cli/help.c:53 msgid " --help-export\t\tExport options" msgstr "" #: ../src/cli/help.c:54 msgid "Main options:" msgstr "" #. Translators: please do not translate '-v, --version' #: ../src/cli/help.c:57 msgid " -v, --version\t\t\t\tShow program version" msgstr "" #. Translators: please do not translate 'show <-a ..> [-i ..] [-m] [-n]' #: ../src/cli/help.c:60 msgid " show <-a ..> [-i ..] [-m] [-n]\tShow a token" msgstr "" #. Translators: please do not translate 'list' #: ../src/cli/help.c:63 msgid " list\t\t\t\t\tList all pairs of account and issuer" msgstr "" #. Translators: please do not translate 'export <-t ..> [-d ..]' #: ../src/cli/help.c:66 msgid " export <-t ..> [-d ..]\t\tExport data" msgstr "" #. Translators: please do not translate '%s show' #: ../src/cli/help.c:74 #, c-format msgid "" "Usage:\n" " %s show <-a ..> [-i ..] [-m]" msgstr "" #. Translators: please do not translate 'show' #: ../src/cli/help.c:80 msgid "show command options:" msgstr "" #. Translators: please do not translate '-a, --account' #: ../src/cli/help.c:82 msgid " -a, --account\t\tThe account name (mandatory)" msgstr "" #. Translators: please do not translate '-i, --issuer' #: ../src/cli/help.c:84 msgid " -i, --issuer\t\tThe issuer name (optional)" msgstr "" #. Translators: please do not translate '-m, --match-exactly' #: ../src/cli/help.c:86 msgid "" " -m, --match-exactly\tShow the token only if it matches exactly the account " "and/or the issuer (optional)" msgstr "" #. Translators: please do not translate '-n, --next' #: ../src/cli/help.c:88 msgid "" " -n, --next\tShow also the next token, not only the current one (optional, " "valid only for TOTP)" msgstr "" #. Translators: please do not translate '%s export' #: ../src/cli/help.c:96 #, c-format msgid "" "Usage:\n" " %s export <-t> [-d ..]" msgstr "" #. Translators: please do not translate 'export' #: ../src/cli/help.c:102 msgid "export command options:" msgstr "" #. Translators: please do not translate '-t, --type' #: ../src/cli/help.c:104 msgid "" " -t, --type\t\tExport format. Must be either one of: andotp_plain, " "andotp_encrypted, freeotpplus, aegis" msgstr "" #. Translators: please do not translate '-d, --directory' #: ../src/cli/help.c:106 msgid "" " -d, --directory\tThe output directory where the exported file will be " "saved." msgstr "" #: ../src/cli/help.c:107 msgid "" "\t\t\tIf nothing is specified OR flatpak is being used, the output folder " "will be the user's HOME directory." msgstr "" #: ../src/cli/get-data.c:58 msgid "" "Couldn't find the data. Either the given data is wrong or is not in the " "database." msgstr "" #. Translators: please do not translate 'account' #: ../src/cli/get-data.c:61 #, c-format msgid "Given account: %s" msgstr "" #. Translators: please do not translate 'issuer' #: ../src/cli/get-data.c:67 #, c-format msgid "Given issuer: %s" msgstr "" #: ../src/cli/get-data.c:127 #, c-format msgid "Current TOTP (valid for %d more second(s)): %s\n" msgstr "" #: ../src/cli/get-data.c:131 #, c-format msgid "Current HOTP: %s\n" msgstr "" OTPClient-3.2.1/po/otpclient.pot000066400000000000000000000240021452112020400164460ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-12-09 14:00+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: ../src/ui/otpclient.ui:18 ../src/ui/otpclient.ui:2257 msgid "From file" msgstr "" #: ../src/ui/otpclient.ui:32 msgid "From clipboard" msgstr "" #: ../src/ui/otpclient.ui:57 ../src/ui/otpclient.ui:593 #: ../src/ui/otpclient.ui:655 msgid "Scan using webcam" msgstr "" #: ../src/ui/otpclient.ui:71 msgid "Using a QR Code" msgstr "" #: ../src/ui/otpclient.ui:87 msgid "Manually" msgstr "" #: ../src/ui/otpclient.ui:106 ../src/ui/otpclient.ui:2158 msgid "Change database" msgstr "" #: ../src/ui/otpclient.ui:120 ../src/ui/otpclient.ui:240 #: ../src/ui/otpclient.ui:381 ../src/ui/otpclient.ui:610 #: ../src/ui/otpclient.ui:671 ../src/ui/otpclient.ui:729 #: ../src/ui/otpclient.ui:838 ../src/ui/otpclient.ui:1131 #: ../src/ui/otpclient.ui:1439 ../src/ui/otpclient.ui:1542 #: ../src/ui/otpclient.ui:1663 msgid "Cancel" msgstr "" #: ../src/ui/otpclient.ui:133 ../src/ui/otpclient.ui:253 #: ../src/ui/otpclient.ui:394 ../src/ui/otpclient.ui:851 #: ../src/ui/otpclient.ui:1144 ../src/ui/otpclient.ui:1452 #: ../src/ui/otpclient.ui:1555 ../src/ui/otpclient.ui:1676 #: ../src/ui/otpclient.ui:2308 ../src/ui/otpclient.ui:2630 msgid "OK" msgstr "" #: ../src/ui/otpclient.ui:161 msgid "" "Use the rightmost icon on the second entry to select a different OTPClient " "database." msgstr "" #: ../src/ui/otpclient.ui:177 ../src/ui/otpclient.ui:194 #: ../src/ui/otpclient.ui:297 ../src/ui/otpclient.ui:315 #: ../src/ui/otpclient.ui:333 ../src/ui/otpclient.ui:432 #: ../src/ui/otpclient.ui:1496 ../src/ui/otpclient.ui:1599 #: ../src/ui/otpclient.ui:1617 ../src/ui/otpclient.ui:2571 msgid "Max 255 chars" msgstr "" #: ../src/ui/otpclient.ui:181 ../src/ui/otpclient.ui:199 #: ../src/ui/otpclient.ui:301 ../src/ui/otpclient.ui:319 #: ../src/ui/otpclient.ui:337 ../src/ui/otpclient.ui:436 #: ../src/ui/otpclient.ui:1501 ../src/ui/otpclient.ui:1603 #: ../src/ui/otpclient.ui:1621 ../src/ui/otpclient.ui:2575 msgid "Show password" msgstr "" #: ../src/ui/otpclient.ui:200 msgid "New DB path..." msgstr "" #: ../src/ui/otpclient.ui:225 ../src/ui/otpclient.ui:2172 msgid "Change password" msgstr "" #: ../src/ui/otpclient.ui:281 msgid "" "Please type the current password, the new one, and again the new one to " "verify its correctness.\n" " Please note that there is no way to recover a forgotten " "password." msgstr "" #: ../src/ui/otpclient.ui:302 msgid "Type current password..." msgstr "" #: ../src/ui/otpclient.ui:320 msgid "Type new password..." msgstr "" #: ../src/ui/otpclient.ui:338 msgid "Retype new password..." msgstr "" #: ../src/ui/otpclient.ui:364 ../src/ui/otpclient.ui:1527 #: ../src/ui/otpclient.ui:2499 msgid "Password" msgstr "" #: ../src/ui/otpclient.ui:437 ../src/ui/otpclient.ui:1604 #: ../src/ui/otpclient.ui:2576 msgid "Type password..." msgstr "" #: ../src/ui/otpclient.ui:468 ../src/ui/otpclient.ui:538 msgid "No" msgstr "" #: ../src/ui/otpclient.ui:481 ../src/ui/otpclient.ui:551 msgid "Yes" msgstr "" #: ../src/ui/otpclient.ui:505 msgid "Are you sure you want to permanently delete the selected item?" msgstr "" #: ../src/ui/otpclient.ui:632 msgid "" "Select the file with the QR code and copy it (CTRL-C).\n" "\n" " If you are using KDE, then you must copy the file " "before selecting this option, otherwise the content won't be parsed.\n" "\n" " This dialog will close automatically as soon as a valid qrcode " "has been found or after 30 seconds if nothing has been detected." msgstr "" #: ../src/ui/otpclient.ui:693 msgid "" "Please place the qrcode in front of the webcam.\n" "This dialog will automatically close as soon as a valid qrcode\n" "has been found or after 30 seconds if nothing has been detected." msgstr "" #: ../src/ui/otpclient.ui:714 msgid "Database location" msgstr "" #: ../src/ui/otpclient.ui:759 msgid "" "This seems to be the first time you run OTPClient on this device.\n" " Please select whether you want to restore an existing " "database or create a new one." msgstr "" #: ../src/ui/otpclient.ui:776 msgid "Do NOT use to import third party databases (e.g. andOTP)" msgstr "" #: ../src/ui/otpclient.ui:781 msgid "Restore existing OTPClient database" msgstr "" #: ../src/ui/otpclient.ui:794 msgid "Create new database" msgstr "" #: ../src/ui/otpclient.ui:823 msgid "Edit data" msgstr "" #: ../src/ui/otpclient.ui:874 msgid "" "Please note that:\n" "- \"account\" is mandatory\n" "- \"issuer\", while optional, is highly recommended\n" "- when changing the \"secret\", be aware that only the secret itself " "can be changed,\n" "but not the number of digits and/or the period/counter." msgstr "" #: ../src/ui/otpclient.ui:918 msgid "Check if you want to edit the \"Account\"" msgstr "" #: ../src/ui/otpclient.ui:948 ../src/ui/otpclient.ui:1234 #: ../src/ui/otpclient.ui:1754 msgid "Account" msgstr "" #: ../src/ui/otpclient.ui:982 msgid "Check if you want to edit the \"Issuer\"" msgstr "" #: ../src/ui/otpclient.ui:1012 ../src/ui/otpclient.ui:1247 #: ../src/ui/otpclient.ui:1248 ../src/ui/otpclient.ui:1755 msgid "Issuer" msgstr "" #: ../src/ui/otpclient.ui:1046 msgid "Check if you want to edit the \"Secret\"" msgstr "" #: ../src/ui/otpclient.ui:1078 ../src/ui/otpclient.ui:1263 #: ../src/ui/otpclient.ui:1264 msgid "Secret" msgstr "" #: ../src/ui/otpclient.ui:1115 msgid "Add Token" msgstr "" #: ../src/ui/otpclient.ui:1180 msgid "TOTP" msgstr "" #: ../src/ui/otpclient.ui:1181 msgid "HOTP" msgstr "" #: ../src/ui/otpclient.ui:1196 msgid "SHA1" msgstr "" #: ../src/ui/otpclient.ui:1197 msgid "SHA256" msgstr "" #: ../src/ui/otpclient.ui:1198 msgid "SHA512" msgstr "" #: ../src/ui/otpclient.ui:1209 msgid "This is a Steam code" msgstr "" #: ../src/ui/otpclient.ui:1233 msgid "Label" msgstr "" #: ../src/ui/otpclient.ui:1289 msgid "Digits" msgstr "" #: ../src/ui/otpclient.ui:1301 msgid "Between 4 and 10" msgstr "" #: ../src/ui/otpclient.ui:1304 msgid "6" msgstr "" #: ../src/ui/otpclient.ui:1330 msgid "Period" msgstr "" #: ../src/ui/otpclient.ui:1342 msgid "In seconds between 10 and 120" msgstr "" #: ../src/ui/otpclient.ui:1345 msgid "30" msgstr "" #: ../src/ui/otpclient.ui:1371 msgid "Counter" msgstr "" #: ../src/ui/otpclient.ui:1383 msgid "Value decided by the server (HOTP only)" msgstr "" #: ../src/ui/otpclient.ui:1425 msgid "New empty database" msgstr "" #: ../src/ui/otpclient.ui:1480 msgid "" "This will create and load a new empty database.\n" " The current database will neither be overwritten " "nor deleted." msgstr "" #: ../src/ui/otpclient.ui:1502 msgid "Full path to the new database..." msgstr "" #: ../src/ui/otpclient.ui:1583 msgid "" "Choose an encryption password for the database.\n" " Please note that there is no way to recover a forgotten " "password." msgstr "" #: ../src/ui/otpclient.ui:1622 msgid "Retype password..." msgstr "" #: ../src/ui/otpclient.ui:1648 ../src/ui/otpclient.ui:2202 #: ../src/ui/otpclient.ui:2459 msgid "Settings" msgstr "" #: ../src/ui/otpclient.ui:1706 msgid "Show next OTP" msgstr "" #: ../src/ui/otpclient.ui:1717 msgid "Disable notifications" msgstr "" #: ../src/ui/otpclient.ui:1728 msgid "Search by" msgstr "" #: ../src/ui/otpclient.ui:1779 msgid "Auto lock on system lock" msgstr "" #: ../src/ui/otpclient.ui:1802 msgid "Auto lock when inactive for" msgstr "" #: ../src/ui/otpclient.ui:1816 msgid "Never" msgstr "" #: ../src/ui/otpclient.ui:1817 msgid "30s" msgstr "" #: ../src/ui/otpclient.ui:1818 msgid "1m" msgstr "" #: ../src/ui/otpclient.ui:1819 msgid "5m" msgstr "" #: ../src/ui/otpclient.ui:1820 msgid "15m" msgstr "" #: ../src/ui/otpclient.ui:1821 msgid "30m" msgstr "" #: ../src/ui/otpclient.ui:1833 msgid "Dark theme enabled" msgstr "" #: ../src/ui/otpclient.ui:1856 msgid "Disable secret service" msgstr "" #: ../src/ui/otpclient.ui:1923 ../src/ui/otpclient.ui:2033 msgid "andOTP (encrypted)" msgstr "" #: ../src/ui/otpclient.ui:1937 ../src/ui/otpclient.ui:2047 msgid "andOTP (plain)" msgstr "" #: ../src/ui/otpclient.ui:1951 msgid "Authenticator Plus" msgstr "" #: ../src/ui/otpclient.ui:1965 ../src/ui/otpclient.ui:2061 msgid "FreeOTP+ (key URI)" msgstr "" #: ../src/ui/otpclient.ui:1979 ../src/ui/otpclient.ui:2089 msgid "Aegis (plain json)" msgstr "" #: ../src/ui/otpclient.ui:1993 ../src/ui/otpclient.ui:2075 msgid "Aegis (encrypted json)" msgstr "" #: ../src/ui/otpclient.ui:2006 msgid "Google Migration QR" msgstr "" #: ../src/ui/otpclient.ui:2113 msgid "Import" msgstr "" #: ../src/ui/otpclient.ui:2128 msgid "Export" msgstr "" #: ../src/ui/otpclient.ui:2144 msgid "New database" msgstr "" #: ../src/ui/otpclient.ui:2187 msgid "Edit row" msgstr "" #: ../src/ui/otpclient.ui:2217 msgid "Keyboard shortcuts" msgstr "" #: ../src/ui/otpclient.ui:2232 msgid "About" msgstr "" #: ../src/ui/otpclient.ui:2271 msgid "From webcam" msgstr "" #: ../src/ui/otpclient.ui:2392 msgid "OTPClient" msgstr "" #: ../src/ui/otpclient.ui:2403 msgid "Add token" msgstr "" #: ../src/ui/otpclient.ui:2426 msgid "Toggle delete rows" msgstr "" #: ../src/ui/otpclient.ui:2440 msgid "Enable/disable rows reordering" msgstr "" #: ../src/ui/otpclient.ui:2514 msgid "Quit" msgstr "" #: ../src/ui/otpclient.ui:2527 msgid "Unlock" msgstr "" #: ../src/ui/otpclient.ui:2555 msgid "" "The application is locked.\n" " Please type your password to unlock it" msgstr "" #: ../src/ui/otpclient.ui:2601 msgid "Warning: memlock value too low" msgstr "" #: ../src/ui/otpclient.ui:2617 msgid "Exit" msgstr "" #: ../src/ui/otpclient.ui:2670 msgid "Do not show this warning again" msgstr "" OTPClient-3.2.1/proto/000077500000000000000000000000001452112020400144505ustar00rootroot00000000000000OTPClient-3.2.1/proto/google-migration.proto000066400000000000000000000013511452112020400210000ustar00rootroot00000000000000syntax = "proto3"; message MigrationPayload { enum Algorithm { ALGORITHM_UNSPECIFIED = 0; ALGORITHM_SHA1 = 1; ALGORITHM_SHA256 = 2; ALGORITHM_SHA512 = 3; ALGORITHM_MD5 = 4; } enum DigitCount { DIGIT_COUNT_UNSPECIFIED = 0; DIGIT_COUNT_SIX = 1; DIGIT_COUNT_EIGHT = 2; } enum OtpType { OTP_TYPE_UNSPECIFIED = 0; OTP_TYPE_HOTP = 1; OTP_TYPE_TOTP = 2; } message OtpParameters { bytes secret = 1; string name = 2; string issuer = 3; Algorithm algorithm = 4; DigitCount digits = 5; OtpType type = 6; int64 counter = 7; } repeated OtpParameters otp_parameters = 1; int32 version = 2; int32 batch_size = 3; int32 batch_index = 4; int32 batch_id = 5; }OTPClient-3.2.1/src/000077500000000000000000000000001452112020400140745ustar00rootroot00000000000000OTPClient-3.2.1/src/about_diag_cb.c000066400000000000000000000035671452112020400170150ustar00rootroot00000000000000#include #include #include "version.h" #include "data.h" void about_diag_cb (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; const gchar *authors[] = {"Paolo Stivanin ", NULL}; const gchar *artists[] = {"Tobias Bernard (bertob) ", NULL}; const gchar *partial_path = "share/icons/hicolor/scalable/apps/com.github.paolostivanin.OTPClient.svg"; gchar *icon_abs_path = g_strconcat (INSTALL_PREFIX, "/", partial_path, NULL); GtkWidget *ab_diag = gtk_about_dialog_new (); gtk_window_set_transient_for (GTK_WINDOW(app_data->main_window), GTK_WINDOW(ab_diag)); gtk_about_dialog_set_program_name (GTK_ABOUT_DIALOG(ab_diag), PROJECT_NAME); gtk_about_dialog_set_version (GTK_ABOUT_DIALOG(ab_diag), PROJECT_VER); gtk_about_dialog_set_copyright (GTK_ABOUT_DIALOG(ab_diag), "2017-2022"); gtk_about_dialog_set_comments (GTK_ABOUT_DIALOG(ab_diag), _("Highly secure and easy to use GTK+ software for two-factor authentication that supports both Time-based One-time Passwords (TOTP) and HMAC-Based One-Time Passwords (HOTP).")); gtk_about_dialog_set_license_type (GTK_ABOUT_DIALOG(ab_diag), GTK_LICENSE_GPL_3_0); gtk_about_dialog_set_website (GTK_ABOUT_DIALOG(ab_diag), "https://github.com/paolostivanin/OTPClient"); gtk_about_dialog_set_authors (GTK_ABOUT_DIALOG(ab_diag), authors); gtk_about_dialog_set_artists (GTK_ABOUT_DIALOG(ab_diag), artists); GdkPixbuf *logo = gdk_pixbuf_new_from_file (icon_abs_path, NULL); gtk_about_dialog_set_logo (GTK_ABOUT_DIALOG(ab_diag), logo); g_free (icon_abs_path); g_signal_connect (ab_diag, "response", G_CALLBACK (gtk_widget_destroy), NULL); gtk_widget_show_all (ab_diag); } OTPClient-3.2.1/src/add-common.c000066400000000000000000000025371452112020400162650ustar00rootroot00000000000000#include #include "data.h" #include "imports.h" #include "parse-uri.h" static gchar *check_params (GSList *otps); gchar * add_data_to_db (const gchar *otp_uri, AppData *app_data) { GSList *otps = NULL; set_otps_from_uris (otp_uri, &otps); if (g_slist_length (otps) != 1) { return g_strdup ("No valid otpauth uris found"); } gchar *err_msg = check_params (otps); if (err_msg != NULL) { return err_msg; } err_msg = update_db_from_otps (otps, app_data); if (err_msg != NULL) { return err_msg; } free_otps_gslist (otps, g_slist_length (otps)); return NULL; } static gchar * check_params (GSList *otps) { otp_t *otp = g_slist_nth_data (otps, 0); if (otp->account_name == NULL) { return g_strdup ("Label can not be empty, otp not imported"); } if (otp->secret == NULL || otp->secret[0] == '\0') { return g_strdup ("Secret can not be empty, otp not imported"); } if (g_ascii_strcasecmp (otp->type, "TOTP") == 0) { if (otp->period < 10 || otp->period > 120) { gchar *msg = g_strconcat("[INFO]: invalid period for '", otp->account_name, "'. Defaulting back to 30 seconds.", NULL); g_printerr ("%s\n", msg); g_free (msg); otp->period = 30; } } return NULL; } OTPClient-3.2.1/src/add-common.h000066400000000000000000000002331452112020400162610ustar00rootroot00000000000000#pragma once #include "data.h" G_BEGIN_DECLS gchar *add_data_to_db (const gchar *otp_uri, AppData *app_data); G_END_DECLS OTPClient-3.2.1/src/add-from-qr.c000066400000000000000000000174741452112020400163660ustar00rootroot00000000000000#include #include #include #include #include "imports.h" #include "qrcode-parser.h" #include "message-dialogs.h" #include "add-common.h" #include "get-builder.h" #include "common/common.h" #include "gui-common.h" typedef struct gtimeout_data_t { GtkWidget *diag; gboolean uris_available; gboolean image_available; gboolean gtimeout_exit_value; guint counter; AppData * app_data; } GTimeoutCBData; static gboolean check_result (gpointer data); static void parse_file_and_update_db (const gchar *filename, AppData *app_data, gboolean google_migration); static void uri_received_func (GtkClipboard *clipboard, gchar **uris, gpointer user_data); static void image_received_func (GtkClipboard *clipboard, GdkPixbuf *pixbuf, gpointer user_data); void add_qr_from_file (GSimpleAction *simple, GVariant *parameter __attribute__((unused)), gpointer user_data) { const gchar *action_name = g_action_get_name (G_ACTION(simple)); gboolean google_migration = (g_strcmp0 (action_name, GOOGLE_MIGRATION_FILE_ACTION_NAME) == 0) ? TRUE : FALSE; AppData *app_data = (AppData *)user_data; GtkFileChooserNative *dialog = gtk_file_chooser_native_new ("Open File", GTK_WINDOW (app_data->main_window), GTK_FILE_CHOOSER_ACTION_OPEN, "Open", "Cancel"); GtkFileFilter *filter = gtk_file_filter_new (); gtk_file_filter_set_name (filter, "QR Image (*.png)"); gtk_file_filter_add_pattern (filter, "*.png"); gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter); gint res = gtk_native_dialog_run (GTK_NATIVE_DIALOG (dialog)); if (res == GTK_RESPONSE_ACCEPT) { gchar *filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); parse_file_and_update_db (filename, app_data, google_migration); g_free (filename); } g_object_unref (dialog); } void add_qr_from_clipboard (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; GTimeoutCBData *gt_cb_data = g_new0 (GTimeoutCBData, 1); gt_cb_data->uris_available = FALSE; gt_cb_data->image_available = FALSE; gt_cb_data->gtimeout_exit_value = TRUE; gt_cb_data->counter = 0; gt_cb_data->app_data = app_data; guint source_id = g_timeout_add (1000, check_result, gt_cb_data); GtkBuilder *builder = get_builder_from_partial_path (UI_PARTIAL_PATH); gt_cb_data->diag = GTK_WIDGET(gtk_builder_get_object (builder, "diag_qr_clipboard_id")); gtk_widget_show_all (gt_cb_data->diag); gint response = gtk_dialog_run (GTK_DIALOG (gt_cb_data->diag)); if (response == GTK_RESPONSE_CANCEL) { if (gt_cb_data->uris_available == TRUE) { gtk_clipboard_request_uris (app_data->clipboard, (GtkClipboardURIReceivedFunc)uri_received_func, app_data); } if (gt_cb_data->image_available == TRUE) { gtk_clipboard_request_image (app_data->clipboard, (GtkClipboardImageReceivedFunc)image_received_func, app_data); } if (gt_cb_data->gtimeout_exit_value == TRUE) { // only remove if 'check_result' returned TRUE g_source_remove (source_id); } gtk_widget_destroy (gt_cb_data->diag); g_free (gt_cb_data); } g_object_unref (builder); } static gboolean check_result (gpointer data) { GTimeoutCBData *gt_cb_data = (GTimeoutCBData *)data; gt_cb_data->uris_available = gtk_clipboard_wait_is_uris_available (gt_cb_data->app_data->clipboard); gt_cb_data->image_available = gtk_clipboard_wait_is_image_available (gt_cb_data->app_data->clipboard); if (gt_cb_data->counter > 30 || gt_cb_data->uris_available == TRUE || gt_cb_data->image_available == TRUE) { gtk_dialog_response (GTK_DIALOG (gt_cb_data->diag), GTK_RESPONSE_CANCEL); gt_cb_data->gtimeout_exit_value = FALSE; return FALSE; } gt_cb_data->counter++; return TRUE; } static void parse_file_and_update_db (const gchar *filename, AppData *app_data, gboolean google_migration) { gchar *otpauth_uri = NULL; gchar *err_msg = parse_qrcode (filename, &otpauth_uri); if (err_msg != NULL) { show_message_dialog(app_data->main_window, err_msg, GTK_MESSAGE_ERROR); g_free(err_msg); return; } err_msg = parse_uris_migration (app_data, otpauth_uri, google_migration); if (err_msg != NULL) { show_message_dialog (app_data->main_window, err_msg, GTK_MESSAGE_ERROR); g_free (err_msg); } else { show_message_dialog (app_data->main_window, "QRCode successfully scanned", GTK_MESSAGE_INFO); } gcry_free (otpauth_uri); } static void uri_received_func (GtkClipboard *clipboard __attribute__((unused)), gchar **uris, gpointer user_data) { AppData *app_data = (AppData *)user_data; GdkPixbuf *pbuf; GError *err = NULL; if (uris != NULL && uris[0] != NULL) { glong len_fpath = g_utf8_strlen (uris[0], -1) - 7 + 1; // -7 is for file:// gchar *file_path = g_malloc0 (len_fpath); memcpy (file_path, uris[0] + 7, len_fpath); pbuf = gdk_pixbuf_new_from_file (file_path, &err); g_free (file_path); if (err != NULL) { gchar *msg = g_strconcat ("Couldn't get QR code URI from clipboard: ", err->message, NULL); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); } else { // here we convert the input file to a PNG file, so we are able to parse it later on. gchar *filename = g_build_filename (g_get_tmp_dir (), "qrcode_from_cb_uri.png", NULL); gdk_pixbuf_save (pbuf, filename, "png", &err, NULL); parse_file_and_update_db (filename, app_data, FALSE); if (g_unlink (filename) == -1) { g_printerr ("%s\n", _("Couldn't unlink the temp pixbuf.")); } g_free (filename); g_object_unref (pbuf); } } else { show_message_dialog (app_data->main_window, "Couldn't get QR code URI from clipboard", GTK_MESSAGE_ERROR); } } static void image_received_func (GtkClipboard *clipboard __attribute__((unused)), GdkPixbuf *pixbuf, gpointer user_data) { AppData *app_data = (AppData *)user_data; GError *err = NULL; if (pixbuf != NULL) { gchar *filename = g_build_filename (g_get_tmp_dir (), "qrcode_from_cb.png", NULL); gdk_pixbuf_save (pixbuf, filename, "png", &err, NULL); if (err != NULL) { gchar *msg = g_strconcat ("Couldn't save clipboard to png:\n", err->message, NULL); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); } else { parse_file_and_update_db (filename, app_data, FALSE); } if (g_unlink (filename) == -1) { g_printerr ("%s\n", _("Error while unlinking the temp png.")); } g_free (filename); } else { show_message_dialog (app_data->main_window, "Couldn't get QR code image from clipboard", GTK_MESSAGE_ERROR); } }OTPClient-3.2.1/src/app.c000066400000000000000000001031521452112020400150220ustar00rootroot00000000000000#include #include #include #include #include #include "otpclient.h" #include "gquarks.h" #include "imports.h" #include "common/exports.h" #include "message-dialogs.h" #include "password-cb.h" #include "get-builder.h" #include "liststore-misc.h" #include "lock-app.h" #include "change-db-cb.h" #include "new-db-cb.h" #include "common/common.h" #include "secret-schema.h" #include "change-pwd-cb.h" #include "settings-cb.h" #include "shortcuts-cb.h" #include "webcam-add-cb.h" #include "manual-add-cb.h" #include "edit-row-cb.h" #include "show-qr-cb.h" #include "dbinfo-cb.h" #ifndef USE_FLATPAK_APP_FOLDER static gchar *get_db_path (AppData *app_data); #endif static void set_config_data (gint *width, gint *height, AppData *app_data); static void migrate_secretservice_kf (AppData *app_data, GKeyFile *kf, gboolean value); static gboolean get_warn_data (void); static void set_warn_data (gboolean show_warning); static void create_main_window (gint width, gint height, AppData *app_data); static gboolean show_upgrade_msg (void); static void set_info_bar (AppData *app_data, const gchar *msg); static void on_bar_response (GtkInfoBar *ib, gint response_id, gpointer user_data); static gboolean set_action_group (GtkBuilder *builder, AppData *app_data); static void get_window_size_cb (GtkWidget *window, GtkAllocation *allocation, gpointer user_data); void setup_kb_shortcuts (AppData *app_data); static void toggle_button_cb (GtkWidget *main_window, gpointer user_data); static void reorder_rows_cb (GtkToggleButton *btn, gpointer user_data); static void del_data_cb (GtkToggleButton *btn, gpointer user_data); static void save_sort_order (GtkTreeView *tree_view); static void save_window_size (gint width, gint height); static void store_data (const gchar *param1_name, gint param1_value, const gchar *param2_name, gint param2_value); static gboolean key_pressed_cb (GtkWidget *window, GdkEventKey *event_key, gpointer user_data); static gboolean show_memlock_warn_dialog (gint32 max_file_size, GtkBuilder *builder); static void set_open_db_action (GtkWidget *btn, gpointer user_data); void activate (GtkApplication *app, gpointer user_data __attribute__((unused))) { gint32 max_file_size = get_max_file_size_from_memlock (); AppData *app_data = g_new0 (AppData, 1); app_data->app_locked = FALSE; gint width = 0, height = 0; app_data->show_next_otp = FALSE; // next otp not shown by default app_data->disable_notifications = FALSE; // notifications enabled by default app_data->search_column = 0; // account app_data->auto_lock = FALSE; // disabled by default app_data->inactivity_timeout = 0; // never app_data->use_dark_theme = FALSE; // light theme by default app_data->use_secret_service = TRUE; // secret service enabled by default app_data->is_reorder_active = FALSE; // when app is started, reorder is not set // open_db_file_action is set only on first startup and not when the db is deleted but the cfg file is there, therefore we need a default action app_data->open_db_file_action = GTK_FILE_CHOOSER_ACTION_SAVE; app_data->builder = get_builder_from_partial_path (UI_PARTIAL_PATH); set_config_data (&width, &height, app_data); app_data->db_data = g_new0 (DatabaseData, 1); app_data->db_data->key_stored = FALSE; // at startup, we don't know whether the key is stored or not create_main_window (width, height, app_data); if (app_data->main_window == NULL) { g_printerr ("%s\n", _("Couldn't locate the ui file, exiting...")); g_free (app_data->db_data); g_application_quit (G_APPLICATION(app)); return; } gtk_application_add_window (GTK_APPLICATION(app), GTK_WINDOW(app_data->main_window)); g_signal_connect (app_data->main_window, "size-allocate", G_CALLBACK(get_window_size_cb), NULL); gchar *init_msg = init_libs (max_file_size); if (init_msg != NULL) { show_message_dialog (app_data->main_window, init_msg, GTK_MESSAGE_ERROR); g_free (init_msg); g_free (app_data->db_data); g_application_quit (G_APPLICATION(app)); return; } #ifdef USE_FLATPAK_APP_FOLDER app_data->db_data->db_path = g_build_filename (g_get_user_data_dir (), "otpclient-db.enc", NULL); // on the first run the cfg file is not created in the flatpak version because we use a non-changeable db path gchar *cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); if (!g_file_test (cfg_file_path, G_FILE_TEST_EXISTS)) { g_file_set_contents (cfg_file_path, "[config]", -1, NULL); } g_free (cfg_file_path); #else if (!g_file_test (g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL), G_FILE_TEST_EXISTS)) { app_data->diag_rcdb = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "dialog_rcdb_id")); GtkWidget *restore_btn = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "diag_rc_restoredb_btn_id")); GtkWidget *create_btn = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "diag_rc_createdb_btn_id")); g_signal_connect (restore_btn, "clicked", G_CALLBACK (set_open_db_action), app_data); g_signal_connect (create_btn, "clicked", G_CALLBACK (set_open_db_action), app_data); gint response = gtk_dialog_run (GTK_DIALOG(app_data->diag_rcdb)); switch (response) { case GTK_RESPONSE_CANCEL: default: gtk_widget_destroy (app_data->diag_rcdb); g_free (app_data->db_data); g_free (app_data); g_application_quit (G_APPLICATION(app)); return; case GTK_RESPONSE_OK: gtk_widget_destroy (app_data->diag_rcdb); } } app_data->db_data->db_path = get_db_path (app_data); if (app_data->db_data->db_path == NULL) { g_free (app_data->db_data); g_free (app_data); g_application_quit (G_APPLICATION(app)); return; } #endif if (max_file_size < LOW_MEMLOCK_VALUE && get_warn_data () == TRUE) { if (show_memlock_warn_dialog (max_file_size, app_data->builder) == TRUE) { g_free (app_data->db_data); g_free (app_data); g_application_quit (G_APPLICATION(app)); return; } } app_data->db_data->max_file_size_from_memlock = max_file_size; app_data->db_data->objects_hash = NULL; app_data->db_data->data_to_add = NULL; // subtract 3 seconds from the current time. Needed for "last_hotp" to be set on the first run app_data->db_data->last_hotp_update = g_date_time_add_seconds (g_date_time_new_now_local (), -(G_TIME_SPAN_SECOND * HOTP_RATE_LIMIT_IN_SEC)); if (app_data->use_secret_service == TRUE) { gchar *pwd = secret_password_lookup_sync (OTPCLIENT_SCHEMA, NULL, NULL, "string", "main_pwd", NULL); if (pwd == NULL) { g_printerr ("%s\n", _("Couldn't find the password in the secret service.")); goto retry; } else { app_data->db_data->key_stored = TRUE; app_data->db_data->key= secure_strdup (pwd); secret_password_free (pwd); } } else { retry: app_data->db_data->key = prompt_for_password (app_data, NULL, NULL, FALSE); if (app_data->db_data->key == NULL) { if (change_file (app_data) == FALSE) { g_free (app_data->db_data); g_free (app_data); g_application_quit (G_APPLICATION(app)); return; } } } GError *err = NULL; load_db (app_data->db_data, &err); if (err != NULL && !g_error_matches (err, missing_file_gquark (), MISSING_FILE_CODE)) { show_message_dialog (app_data->main_window, err->message, GTK_MESSAGE_ERROR); gcry_free (app_data->db_data->key); if (g_error_matches (err, memlock_error_gquark (), MEMLOCK_ERRCODE)) { g_free (app_data->db_data); g_free (app_data); g_clear_error (&err); g_application_quit (G_APPLICATION(app)); return; } g_clear_error (&err); goto retry; } if (app_data->use_secret_service == TRUE && app_data->db_data->key_stored == FALSE) { secret_password_store (OTPCLIENT_SCHEMA, SECRET_COLLECTION_DEFAULT, "main_pwd", app_data->db_data->key, NULL, on_password_stored, NULL, "string", "main_pwd", NULL); } if (g_error_matches (err, missing_file_gquark(), MISSING_FILE_CODE)) { const gchar *msg = _("This is the first time you run OTPClient, so you need to add or import some tokens.\n" "- to add tokens, please click the + button on the top left.\n" "- to import existing tokens, please click the menu button on the top right.\n" "\nIf you need more info, please visit the project's wiki"); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_INFO); GError *tmp_err = NULL; update_and_reload_db (app_data, app_data->db_data, FALSE, &tmp_err); g_clear_error (&tmp_err); } app_data->clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); create_treeview (app_data); setup_kb_shortcuts (app_data); app_data->notification = g_notification_new ("OTPClient"); g_notification_set_priority (app_data->notification, G_NOTIFICATION_PRIORITY_NORMAL); GIcon *icon = g_themed_icon_new ("com.github.paolostivanin.OTPClient"); g_notification_set_icon (app_data->notification, icon); g_notification_set_body (app_data->notification, _("OTP value has been copied to the clipboard")); g_object_unref (icon); GtkToggleButton *reorder_toggle_btn = GTK_TOGGLE_BUTTON(gtk_builder_get_object (app_data->builder, "reorder_toggle_btn_id")); g_signal_connect (app_data->main_window, "toggle-reorder-button", G_CALLBACK(toggle_button_cb), reorder_toggle_btn); g_signal_connect (reorder_toggle_btn, "toggled", G_CALLBACK(reorder_rows_cb), app_data); g_signal_connect (app_data->main_window, "key_press_event", G_CALLBACK(key_pressed_cb), NULL); GtkToggleButton *del_toggle_btn = GTK_TOGGLE_BUTTON(gtk_builder_get_object (app_data->builder, "del_toggle_btn_id")); g_signal_connect (app_data->main_window, "toggle-delete-button", G_CALLBACK(toggle_button_cb), del_toggle_btn); g_signal_connect (del_toggle_btn, "toggled", G_CALLBACK(del_data_cb), app_data); g_signal_connect (app_data->main_window, "key_press_event", G_CALLBACK(key_pressed_cb), NULL); g_signal_connect (app_data->main_window, "destroy", G_CALLBACK(destroy_cb), app_data); app_data->source_id = g_timeout_add_full (G_PRIORITY_DEFAULT, 1000, traverse_liststore, app_data, NULL); setup_dbus_listener (app_data); // set last user activity to now, so we have a starting point for the autolock feature app_data->last_user_activity = g_date_time_new_now_local (); app_data->source_id_last_activity = g_timeout_add_seconds (1, check_inactivity, app_data); gtk_widget_show_all (app_data->main_window); app_data->info_bar = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "info_bar_id")); if (show_upgrade_msg ()) { set_info_bar (app_data, _("Not asking for password? Please check the 'Secret Service Integration' new feature HERE")); } else { gtk_widget_hide (app_data->info_bar); } } static gboolean show_memlock_warn_dialog (gint32 max_file_size, GtkBuilder *builder) { gchar *msg = g_strdup_printf (_("Your OS's memlock limit (%d) may be too low for you. " "This could crash the program when importing data from 3rd party apps " "or when a certain amount of tokens is reached. " "Please have a look at the secure memory wiki page before " "using this software with the current settings."), max_file_size); GtkWidget *warn_diag = GTK_WIDGET(gtk_builder_get_object (builder, "warning_diag_id")); GtkLabel *warn_label = GTK_LABEL(gtk_builder_get_object (builder, "warning_diag_label_id")); GtkWidget *warn_chk_btn = GTK_WIDGET(gtk_builder_get_object (builder, "warning_diag_check_btn_id")); gtk_label_set_label (warn_label, msg); gtk_widget_show_all (warn_diag); gboolean quit = FALSE; gint result = gtk_dialog_run (GTK_DIALOG (warn_diag)); switch (result) { case GTK_RESPONSE_OK: set_warn_data (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(warn_chk_btn))); break; case GTK_RESPONSE_CLOSE: default: set_warn_data (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(warn_chk_btn))); quit = TRUE; break; } gtk_widget_destroy (warn_diag); g_free (msg); return quit; } static gboolean key_pressed_cb (GtkWidget *window, GdkEventKey *event_key, gpointer user_data __attribute__((unused))) { switch (event_key->keyval) { case GDK_KEY_q: if (event_key->state & GDK_CONTROL_MASK) { gtk_window_close (GTK_WINDOW(window)); } break; } return FALSE; } static void set_config_data (gint *width, gint *height, AppData *app_data) { GKeyFile *kf = get_kf_ptr (); GError *err = NULL; gboolean tmp; if (kf != NULL) { *width = g_key_file_get_integer (kf, "config", "window_width", NULL); *height = g_key_file_get_integer (kf, "config", "window_height", NULL); app_data->show_next_otp = g_key_file_get_boolean (kf, "config", "show_next_otp", NULL); app_data->disable_notifications = g_key_file_get_boolean (kf, "config", "notifications", NULL); app_data->search_column = g_key_file_get_integer (kf, "config", "search_column", NULL); app_data->auto_lock = g_key_file_get_boolean (kf, "config", "auto_lock", NULL); app_data->inactivity_timeout = g_key_file_get_integer (kf, "config", "inactivity_timeout", NULL); app_data->use_dark_theme = g_key_file_get_boolean (kf, "config", "dark_theme", NULL); // handle migration from disable_secret_service to use_secret_service tmp = g_key_file_get_boolean (kf, "config", "disable_secret_service", &err); if (tmp == TRUE || (tmp == FALSE && err == NULL)) { // old key was found, so we need to migrate to the new format migrate_secretservice_kf (app_data, kf, !tmp); } if (tmp == FALSE && err != NULL) { // key was not found, so we already migrated to the new format app_data->use_secret_service = g_key_file_get_boolean (kf, "config", "use_secret_service", NULL); } // end migration g_object_set (gtk_settings_get_default (), "gtk-application-prefer-dark-theme", app_data->use_dark_theme, NULL); g_key_file_free (kf); } } static void migrate_secretservice_kf (AppData *app_data, GKeyFile *kf, gboolean value) { GError *err = NULL; app_data->use_secret_service = value; g_key_file_set_boolean (kf, "config", "use_secret_service", app_data->use_secret_service); g_key_file_remove_key (kf, "config", "disable_secret_service", NULL); gchar *cfg_file_path; #ifndef USE_FLATPAK_APP_FOLDER cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); #else cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); #endif if (!g_key_file_save_to_file (kf, cfg_file_path, &err)) { gchar *err_msg = g_strconcat (_("Couldn't save the config file: "), err->message, NULL); show_message_dialog (app_data->main_window, err_msg, GTK_MESSAGE_ERROR); g_free (err_msg); g_clear_error (&err); } } static gboolean get_warn_data (void) { GKeyFile *kf = get_kf_ptr (); gboolean show_warning = TRUE; GError *err = NULL; if (kf != NULL) { show_warning = g_key_file_get_boolean (kf, "config", "show_memlock_warning", &err); if (err != NULL && (err->code == G_KEY_FILE_ERROR_KEY_NOT_FOUND || err->code == G_KEY_FILE_ERROR_INVALID_VALUE)) { // value is not present, so we want to show the warning show_warning = TRUE; } g_key_file_free (kf); } return show_warning; } static void set_warn_data (gboolean show_warning) { GKeyFile *kf = get_kf_ptr (); GError *err = NULL; if (kf != NULL) { g_key_file_set_boolean (kf, "config", "show_memlock_warning", show_warning); gchar *cfg_file_path; #ifndef USE_FLATPAK_APP_FOLDER cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); #else cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); #endif if (!g_key_file_save_to_file (kf, cfg_file_path, &err)) { g_printerr ("%s\n", err->message); g_clear_error (&err); } g_free (cfg_file_path); g_key_file_free (kf); } } static void create_main_window (gint width, gint height, AppData *app_data) { app_data->main_window = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "appwindow_id")); gtk_window_set_icon_name (GTK_WINDOW(app_data->main_window), "otpclient"); gtk_window_set_default_size (GTK_WINDOW(app_data->main_window), (width >= 150) ? width : 500, (height >= 150) ? height : 300); GtkWidget *lock_btn = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "lock_btn_id")); g_signal_connect (lock_btn, "clicked", G_CALLBACK(lock_app), app_data); if (app_data->use_secret_service == TRUE) { // secret service is enabled, so we can't lock the app gtk_widget_set_sensitive (lock_btn, FALSE); } set_action_group (app_data->builder, app_data); } static gboolean show_upgrade_msg (void) { gboolean show_msg = TRUE; GKeyFile *kf = get_kf_ptr (); if (kf != NULL) { gchar *up_msg = g_key_file_get_string (kf, "config", "upgrade_msg", NULL); if (up_msg == NULL) { show_msg = TRUE; } else { show_msg = (g_strcmp0 (up_msg, "v2_6") == 0) ? FALSE : TRUE; } } g_key_file_free (kf); return show_msg; } static void set_info_bar (AppData *app_data, const gchar *msg) { GtkWidget *label = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "info_bar_label_id")); g_signal_connect (app_data->info_bar, "response", G_CALLBACK(on_bar_response), NULL); gtk_label_set_markup (GTK_LABEL(label), msg); gtk_info_bar_set_message_type (GTK_INFO_BAR(app_data->info_bar), GTK_MESSAGE_INFO); gtk_widget_show (app_data->info_bar); } static void on_bar_response (GtkInfoBar *ib, gint response_id __attribute__((unused)), gpointer user_data __attribute__((unused))) { GError *err = NULL; GKeyFile *kf = get_kf_ptr (); if (kf != NULL) { g_key_file_set_string (kf, "config", "upgrade_msg", "v2_6"); gchar *cfg_file_path; #ifndef USE_FLATPAK_APP_FOLDER cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); #else cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); #endif if (!g_key_file_save_to_file (kf, cfg_file_path, &err)) { g_printerr ("%s\n", err->message); g_clear_error (&err); } g_free (cfg_file_path); } g_key_file_free (kf); gtk_widget_hide (GTK_WIDGET(ib)); } static gboolean set_action_group (GtkBuilder *builder, AppData *app_data) { static GActionEntry settings_menu_entries[] = { { .name = ANDOTP_IMPORT_ACTION_NAME, .activate = select_file_cb }, { .name = ANDOTP_IMPORT_PLAIN_ACTION_NAME, .activate = select_file_cb }, { .name = FREEOTPPLUS_IMPORT_ACTION_NAME, .activate = select_file_cb }, { .name = AEGIS_IMPORT_ACTION_NAME, .activate = select_file_cb }, { .name = AEGIS_IMPORT_ENC_ACTION_NAME, .activate = select_file_cb }, { .name = ANDOTP_EXPORT_ACTION_NAME, .activate = export_data_cb }, { .name = ANDOTP_EXPORT_PLAIN_ACTION_NAME, .activate = export_data_cb }, { .name = FREEOTPPLUS_EXPORT_ACTION_NAME, .activate = export_data_cb }, { .name = AEGIS_EXPORT_ACTION_NAME, .activate = export_data_cb }, { .name = AEGIS_EXPORT_PLAIN_ACTION_NAME, .activate = export_data_cb }, { .name = GOOGLE_MIGRATION_FILE_ACTION_NAME, .activate = add_qr_from_file }, { .name = GOOGLE_MIGRATION_WEBCAM_ACTION_NAME, .activate = webcam_add_cb }, { .name = "create_newdb", .activate = new_db_cb }, { .name = "change_db", .activate = change_db_cb }, { .name = "change_pwd", .activate = change_password_cb }, { .name = "edit_row", .activate = edit_row_cb }, { .name = "show_qr", .activate = show_qr_cb }, { .name = "settings", .activate = settings_dialog_cb }, { .name = "shortcuts", .activate = shortcuts_window_cb }, { .name = "dbinfo", .activate = dbinfo_cb }, { .name = "about", .activate = about_diag_cb } }; static GActionEntry add_menu_entries[] = { { .name = "webcam", .activate = webcam_add_cb }, { .name = "import_qr_file", .activate = add_qr_from_file }, { .name = "import_qr_clipboard", .activate = add_qr_from_clipboard }, { .name = "manual", .activate = manual_add_cb } }; GtkWidget *settings_popover = GTK_WIDGET (gtk_builder_get_object (builder, "settings_pop_id")); GActionGroup *settings_actions = (GActionGroup *)g_simple_action_group_new (); g_action_map_add_action_entries (G_ACTION_MAP (settings_actions), settings_menu_entries, G_N_ELEMENTS (settings_menu_entries), app_data); gtk_widget_insert_action_group (settings_popover, "settings_menu", settings_actions); GtkWidget *add_popover = GTK_WIDGET (gtk_builder_get_object (builder, "add_pop_id")); GActionGroup *add_actions = (GActionGroup *)g_simple_action_group_new (); g_action_map_add_action_entries (G_ACTION_MAP (add_actions), add_menu_entries, G_N_ELEMENTS (add_menu_entries), app_data); gtk_widget_insert_action_group (add_popover, "add_menu", add_actions); gtk_popover_set_constrain_to (GTK_POPOVER(add_popover), GTK_POPOVER_CONSTRAINT_NONE); gtk_popover_set_constrain_to (GTK_POPOVER(settings_popover), GTK_POPOVER_CONSTRAINT_NONE); return TRUE; } #ifndef USE_FLATPAK_APP_FOLDER static gchar * get_db_path (AppData *app_data) { gchar *db_path = NULL; GError *err = NULL; GKeyFile *kf = g_key_file_new (); gchar *cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); if (g_file_test (cfg_file_path, G_FILE_TEST_EXISTS)) { if (!g_key_file_load_from_file (kf, cfg_file_path, G_KEY_FILE_NONE, &err)) { show_message_dialog (app_data->main_window, err->message, GTK_MESSAGE_ERROR); g_key_file_free (kf); g_clear_error (&err); return NULL; } db_path = g_key_file_get_string (kf, "config", "db_path", NULL); if (db_path == NULL) { goto new_db; } if (!g_file_test (db_path, G_FILE_TEST_EXISTS)) { gchar *msg = g_strconcat ("Database file/location:\n", db_path, "\ndoes not exist. A new database will be created.", NULL); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); goto new_db; } goto end; } new_db: ; // empty statement workaround GtkFileChooserNative *dialog = gtk_file_chooser_native_new (_("Select database location"), GTK_WINDOW(app_data->main_window), app_data->open_db_file_action, "OK", "Cancel"); GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog); gtk_file_chooser_set_do_overwrite_confirmation (chooser, TRUE); gtk_file_chooser_set_select_multiple (chooser, FALSE); if (app_data->open_db_file_action == GTK_FILE_CHOOSER_ACTION_SAVE) { gtk_file_chooser_set_current_name (chooser, "NewDatabase.enc"); } gint res = gtk_native_dialog_run (GTK_NATIVE_DIALOG(dialog)); if (res == GTK_RESPONSE_ACCEPT) { db_path = gtk_file_chooser_get_filename (chooser); g_key_file_set_string (kf, "config", "db_path", db_path); if (!g_key_file_save_to_file (kf, cfg_file_path, &err)) { g_printerr ("%s\n", err->message); g_clear_error (&err); } } // clear any password that may have been previously set, thus avoiding using a wrong password with a new database secret_password_clear (OTPCLIENT_SCHEMA, NULL, on_password_cleared, NULL, "string", "main_pwd", NULL); g_object_unref (dialog); end: g_free (cfg_file_path); g_key_file_free (kf); return db_path; } #endif static void toggle_button_cb (GtkWidget *main_window __attribute__((unused)), gpointer user_data) { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON(user_data), !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(user_data))); } static void reorder_rows_cb (GtkToggleButton *btn, gpointer user_data) { AppData *app_data = (AppData *)user_data; gboolean is_btn_active = gtk_toggle_button_get_active (btn); gtk_tree_view_set_reorderable (GTK_TREE_VIEW(app_data->tree_view), is_btn_active); app_data->is_reorder_active = is_btn_active; gtk_widget_set_sensitive (GTK_WIDGET(gtk_builder_get_object (app_data->builder, "add_btn_main_id")), !is_btn_active); gtk_widget_set_sensitive (GTK_WIDGET(gtk_builder_get_object (app_data->builder, "del_toggle_btn_id")), !is_btn_active); if (is_btn_active == FALSE) { // reordering has been disabled, so now we have to reorder and update the database itself reorder_db (app_data); } } static void del_data_cb (GtkToggleButton *btn, gpointer user_data) { AppData *app_data = (AppData *)user_data; GtkStyleContext *gsc = gtk_widget_get_style_context (GTK_WIDGET(btn)); GtkTreeSelection *tree_selection = gtk_tree_view_get_selection (app_data->tree_view); if (gtk_toggle_button_get_active (btn)) { app_data->css_provider = gtk_css_provider_new (); gtk_css_provider_load_from_data (app_data->css_provider, "#delbtn { background: #ff0033; }", -1, NULL); gtk_style_context_add_provider (gsc, GTK_STYLE_PROVIDER(app_data->css_provider), GTK_STYLE_PROVIDER_PRIORITY_USER); const gchar *msg = _("You just entered the deletion mode. You can now click on the row(s) you'd like to delete.\n" "Please note that once a row has been deleted, it's impossible to recover the associated data."); if (get_confirmation_from_dialog (app_data->main_window, msg)) { g_signal_handlers_disconnect_by_func (app_data->tree_view, row_selected_cb, app_data); // the following function emits the "changed" signal gtk_tree_selection_unselect_all (tree_selection); // clear all active otps before proceeding to the deletion phase g_signal_emit_by_name (app_data->tree_view, "hide-all-otps"); g_signal_connect (app_data->tree_view, "row-activated", G_CALLBACK(delete_rows_cb), app_data); } else { gtk_toggle_button_set_active (btn, FALSE); } } else { gtk_style_context_remove_provider (gsc, GTK_STYLE_PROVIDER(app_data->css_provider)); g_object_unref (app_data->css_provider); g_signal_handlers_disconnect_by_func (app_data->tree_view, delete_rows_cb, app_data); g_signal_connect (app_data->tree_view, "row-activated", G_CALLBACK(row_selected_cb), app_data); } } static void get_window_size_cb (GtkWidget *window, GtkAllocation *allocation __attribute__((unused)), gpointer user_data __attribute__((unused))) { gint w, h; gtk_window_get_size (GTK_WINDOW(window), &w, &h); g_object_set_data (G_OBJECT(window), "width", GINT_TO_POINTER(w)); g_object_set_data (G_OBJECT(window), "height", GINT_TO_POINTER(h)); } void destroy_cb (GtkWidget *window, gpointer user_data) { AppData *app_data = (AppData *)user_data; save_sort_order (app_data->tree_view); g_source_remove (app_data->source_id); g_source_remove (app_data->source_id_last_activity); g_date_time_unref (app_data->last_user_activity); for (gint i = 0; i < DBUS_SERVICES; i++) { g_dbus_connection_signal_unsubscribe (app_data->connection, app_data->subscription_ids[i]); } g_dbus_connection_close (app_data->connection, NULL, NULL, NULL); gcry_free (app_data->db_data->key); g_free (app_data->db_data->db_path); g_slist_free_full (app_data->db_data->objects_hash, g_free); json_decref (app_data->db_data->json_data); g_free (app_data->db_data); gtk_clipboard_clear (app_data->clipboard); g_application_withdraw_notification (G_APPLICATION(gtk_window_get_application (GTK_WINDOW(app_data->main_window))), NOTIFICATION_ID); g_object_unref (app_data->notification); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wbad-function-cast" gint w = GPOINTER_TO_INT(g_object_get_data (G_OBJECT(window), "width")); gint h = GPOINTER_TO_INT(g_object_get_data (G_OBJECT(window), "height")); #pragma GCC diagnostic pop save_window_size (w, h); g_object_unref (app_data->builder); g_free (app_data); gcry_control (GCRYCTL_TERM_SECMEM); } static void save_sort_order (GtkTreeView *tree_view) { gint id; GtkSortType order; gtk_tree_sortable_get_sort_column_id (GTK_TREE_SORTABLE(GTK_LIST_STORE(gtk_tree_view_get_model (tree_view))), &id, &order); // store data only if it was changed if (id >= 0) { store_data ("column_id", id, "sort_order", order); } } static void save_window_size (gint width, gint height) { store_data ("window_width", width, "window_height", height); } static void store_data (const gchar *param1_name, gint param1_value, const gchar *param2_name, gint param2_value) { GError *err = NULL; GKeyFile *kf = g_key_file_new (); gchar *cfg_file_path; #ifndef USE_FLATPAK_APP_FOLDER cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); #else cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); #endif if (g_file_test (cfg_file_path, G_FILE_TEST_EXISTS)) { if (!g_key_file_load_from_file (kf, cfg_file_path, G_KEY_FILE_NONE, &err)) { g_printerr ("%s\n", err->message); g_clear_error (&err); } else { g_key_file_set_integer (kf, "config", param1_name, param1_value); g_key_file_set_integer (kf, "config", param2_name, param2_value); if (!g_key_file_save_to_file (kf, cfg_file_path, &err)) { g_printerr ("%s\n", err->message); g_clear_error (&err); } } } g_key_file_free (kf); g_free (cfg_file_path); } static void set_open_db_action (GtkWidget *btn, gpointer user_data) { AppData *app_data = (AppData *)user_data; app_data->open_db_file_action = g_strcmp0 (gtk_widget_get_name (btn), "diag_rc_restoredb_btn") == 0 ? GTK_FILE_CHOOSER_ACTION_OPEN : GTK_FILE_CHOOSER_ACTION_SAVE; gtk_dialog_response (GTK_DIALOG(app_data->diag_rcdb), GTK_RESPONSE_OK); } OTPClient-3.2.1/src/change-db-cb.c000066400000000000000000000051661452112020400164420ustar00rootroot00000000000000#include #include #include #include "data.h" #include "message-dialogs.h" #include "db-misc.h" #include "password-cb.h" #include "db-actions.h" #include "secret-schema.h" void change_db_cb (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; GtkWidget *changedb_diag = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "changedb_diag_id")); GtkWidget *old_changedb_entry = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "changedb_olddb_entry_id")); GtkWidget *new_changedb_entry = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "changedb_entry_id")); g_object_set_data (G_OBJECT(new_changedb_entry), "action", GINT_TO_POINTER(ACTION_OPEN)); g_signal_connect (new_changedb_entry, "icon-press", G_CALLBACK (select_file_icon_pressed_cb), app_data); gtk_entry_set_text (GTK_ENTRY(old_changedb_entry), app_data->db_data->db_path); const gchar *new_db_path; gint result = gtk_dialog_run (GTK_DIALOG (changedb_diag)); switch (result) { case GTK_RESPONSE_OK: new_db_path = gtk_entry_get_text (GTK_ENTRY(new_changedb_entry)); if (!g_file_test (new_db_path, G_FILE_TEST_IS_REGULAR) || g_file_test (new_db_path,G_FILE_TEST_IS_SYMLINK)){ show_message_dialog (app_data->main_window, "Selected file is either a symlink or a non regular file.\nPlease choose another file.", GTK_MESSAGE_ERROR); } else { g_free (app_data->db_data->db_path); app_data->db_data->db_path = g_strdup (new_db_path); update_cfg_file (app_data); gcry_free (app_data->db_data->key); app_data->db_data->key = prompt_for_password (app_data, NULL, NULL, FALSE); secret_password_store (OTPCLIENT_SCHEMA, SECRET_COLLECTION_DEFAULT, "main_pwd", app_data->db_data->key, NULL, on_password_stored, NULL, "string", "main_pwd", NULL); GError *err = NULL; load_new_db (app_data, &err); if (err != NULL) { show_message_dialog (app_data->main_window, err->message, GTK_MESSAGE_ERROR); g_clear_error (&err); } } break; case GTK_RESPONSE_CANCEL: default: break; } gtk_widget_destroy (changedb_diag); } void change_db_cb_shortcut (GtkWidget *w __attribute__((unused)), gpointer user_data) { change_db_cb (NULL, NULL, user_data); } OTPClient-3.2.1/src/change-db-cb.h000066400000000000000000000005061452112020400164400ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS void change_db_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void change_db_cb_shortcut (GtkWidget *w, gpointer user_data); G_END_DECLS OTPClient-3.2.1/src/change-file-cb.c000066400000000000000000000025141452112020400167660ustar00rootroot00000000000000#include #include "new-db-cb.h" #include "change-db-cb.h" #include "db-misc.h" #include "message-dialogs.h" gboolean change_file (AppData *app_data) { GtkWidget *label = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "diag_changefile_label_id")); gchar *partial_msg_start = g_markup_printf_escaped ("%s %s", "The currently selected file is:\n", app_data->db_data->db_path); const gchar *partial_msg_end = "\n\nWhat would you like to do?"; gchar *msg = g_strconcat (partial_msg_start, partial_msg_end, NULL); gtk_label_set_markup (GTK_LABEL(label), msg); g_free (msg); g_free (partial_msg_start); gboolean res = FALSE; GtkWidget *diag_changefile = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "diag_changefile_id")); gint result = gtk_dialog_run (GTK_DIALOG(diag_changefile)); switch (result) { case GTK_RESPONSE_ACCEPT: // select an existing DB. change_db_cb (NULL, NULL, app_data); res = TRUE; break; case GTK_RESPONSE_OK: // create a new db. new_db_cb (NULL, NULL, app_data); res = TRUE; break; case GTK_RESPONSE_CANCEL: default: break; } gtk_widget_hide (diag_changefile); return res; } OTPClient-3.2.1/src/change-pwd-cb.c000066400000000000000000000031311452112020400166350ustar00rootroot00000000000000#include #include #include #include "data.h" #include "message-dialogs.h" #include "db-misc.h" #include "password-cb.h" #include "common/common.h" #include "secret-schema.h" #include "otpclient.h" void change_password_cb (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; gchar *tmp_key = secure_strdup (app_data->db_data->key); gchar *pwd = prompt_for_password (app_data, tmp_key, NULL, FALSE); if (pwd != NULL) { app_data->db_data->key = pwd; GError *err = NULL; update_and_reload_db (app_data, app_data->db_data, FALSE, &err); if (err != NULL) { show_message_dialog (app_data->main_window, err->message, GTK_MESSAGE_ERROR); GtkApplication *app = gtk_window_get_application (GTK_WINDOW(app_data->main_window)); destroy_cb (app_data->main_window, app_data); g_application_quit (G_APPLICATION(app)); return; } show_message_dialog (app_data->main_window, "Password successfully changed", GTK_MESSAGE_INFO); secret_password_store (OTPCLIENT_SCHEMA, SECRET_COLLECTION_DEFAULT, "main_pwd", app_data->db_data->key, NULL, on_password_stored, NULL, "string", "main_pwd", NULL); } else { gcry_free (tmp_key); } } void change_pwd_cb_shortcut (GtkWidget *w __attribute__((unused)), gpointer user_data) { change_password_cb (NULL, NULL, user_data); }OTPClient-3.2.1/src/change-pwd-cb.h000066400000000000000000000005121452112020400166420ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS void change_password_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void change_pwd_cb_shortcut (GtkWidget *w, gpointer user_data); G_END_DECLSOTPClient-3.2.1/src/cli/000077500000000000000000000000001452112020400146435ustar00rootroot00000000000000OTPClient-3.2.1/src/cli/get-data.c000066400000000000000000000134471452112020400165060ustar00rootroot00000000000000#include #include #include #include #include "../db-misc.h" #include "../common/common.h" static gint compare_strings (const gchar *s1, const gchar *s2, gboolean match_exactly); static void get_token (json_t *obj, DatabaseData *db_data, gboolean show_next_token); void show_token (DatabaseData *db_data, const gchar *account, const gchar *issuer, gboolean match_exactly, gboolean show_next_token) { gsize index; json_t *obj; gboolean found = FALSE; json_array_foreach (db_data->json_data, index, obj) { const gchar *account_from_db = json_string_value (json_object_get (obj, "label")); const gchar *issuer_from_db = NULL; if (issuer != NULL) { issuer_from_db = json_string_value (json_object_get (obj, "issuer")); } if (account_from_db != NULL && issuer_from_db != NULL && account != NULL) { // both account and issuer are present if (compare_strings (account_from_db, account, match_exactly) == 0 && compare_strings (issuer_from_db, issuer, match_exactly) == 0) { get_token (obj, db_data, show_next_token); found = TRUE; } } else { if (account_from_db != NULL && account != NULL) { // account is present, but issuer is not if (compare_strings (account_from_db, account, match_exactly) == 0) { get_token (obj, db_data, show_next_token); found = TRUE; } } else { // account was null, but issue may be present if (issuer_from_db != NULL) { if (compare_strings (issuer_from_db, issuer, match_exactly) == 0) { get_token (obj, db_data, show_next_token); found = TRUE; } } } } } if (!found) { g_printerr ("%s\n", _("Couldn't find the data. Either the given data is wrong or is not in the database.")); // Translators: please do not translate 'account' GString *msg = g_string_new (_("Given account: %s")); #if GLIB_CHECK_VERSION(2, 68, 0) g_string_replace (msg, "%s", account != NULL ? account : "", 0); #else g_string_replace_backported (msg, "%s", account != NULL ? account : "", 0); #endif g_printerr ("%s\n", msg->str); g_string_free (msg, TRUE); // Translators: please do not translate 'issuer' msg = g_string_new (_("Given issuer: %s")); #if GLIB_CHECK_VERSION(2, 68, 0) g_string_replace (msg, "%s", issuer != NULL ? issuer : "", 0); #else g_string_replace_backported (msg, "%s", issuer != NULL ? issuer : "", 0); #endif g_printerr ("%s\n", msg->str); g_string_free (msg, TRUE); return; } } void list_all_acc_iss (DatabaseData *db_data) { gsize index; json_t *obj; g_print ("================\n"); g_print ("Account | Issuer\n"); g_print ("================\n"); json_array_foreach (db_data->json_data, index, obj) { g_print ("%s | %s\n", json_string_value (json_object_get (obj, "label")), json_string_value (json_object_get (obj, "issuer"))); g_print ("----------------\n"); } } static gint compare_strings (const gchar *s1, const gchar *s2, gboolean match_exactly) { return match_exactly ? g_strcmp0 (s1, s2) : g_ascii_strcasecmp (s1, s2); } static void get_token (json_t *obj, DatabaseData *db_data, gboolean show_next_token) { cotp_error_t cotp_err; const gchar *issuer = json_string_value (json_object_get (obj, "issuer")); const gchar *secret = json_string_value (json_object_get (obj, "secret")); gint digits = (gint)json_integer_value (json_object_get (obj, "digits")); gint algo = get_algo_int_from_str (json_string_value (json_object_get (obj, "algo"))); gint period; gint64 counter; if (g_ascii_strcasecmp (json_string_value (json_object_get (obj, "type")), "TOTP") == 0) { period = (gint)json_integer_value (json_object_get (obj, "period")); gint remaining_seconds = (period > 59 ? 119 : 59) - g_date_time_get_second (g_date_time_new_now_local()); gint token_validity = remaining_seconds % period; glong current_ts = time(NULL); gchar *current_totp = NULL; gchar *next_totp = NULL; if ((issuer != NULL && g_ascii_strcasecmp (issuer, "steam") == 0) ? TRUE : FALSE) { current_totp = get_steam_totp_at (secret, current_ts, period, &cotp_err); if (show_next_token) next_totp = get_steam_totp_at (secret, current_ts + period, period, &cotp_err); } else { current_totp = get_totp_at (secret, current_ts, digits, period, algo, &cotp_err); if (show_next_token) next_totp = get_totp_at (secret, current_ts + period, digits, period, algo, &cotp_err); } g_print (_("Current TOTP (valid for %d more second(s)): %s\n"), token_validity, current_totp); if (show_next_token) g_print ("Next TOTP: %s\n", next_totp); } else { counter = json_integer_value (json_object_get (obj, "counter")); g_print (_("Current HOTP: %s\n"), get_hotp (secret, counter, digits, algo, &cotp_err)); // counter must be updated every time it is accessed json_object_set (obj, "counter", json_integer (counter + 1)); GError *err = NULL; update_and_reload_db (NULL, db_data, FALSE, &err); if (err != NULL) { g_printerr ("[ERROR] %s\n", err->message); } } } OTPClient-3.2.1/src/cli/get-data.h000066400000000000000000000006061452112020400165040ustar00rootroot00000000000000#pragma once #include #include "../db-misc.h" G_BEGIN_DECLS void show_token (DatabaseData *db_data, const gchar *account, const gchar *issuer, gboolean match_exactly, gboolean show_next_token); void list_all_acc_iss (DatabaseData *db_data); G_END_DECLS OTPClient-3.2.1/src/cli/help.c000066400000000000000000000114161452112020400157420ustar00rootroot00000000000000#include #include #include "version.h" #include "../common/common.h" static void print_main_help (const gchar *prg_name); static void print_show_help (const gchar *prg_name); static void print_export_help (const gchar *prg_name); gboolean show_help (const gchar *prg_name, const gchar *help_command) { gboolean help_displayed = FALSE; if (g_strcmp0 (help_command, "-h") == 0 || g_strcmp0 (help_command, "--help") == 0 || g_strcmp0 (help_command, "help") == 0 || help_command == NULL || g_utf8_strlen (help_command, -1) < 2) { print_main_help (prg_name); help_displayed = TRUE; } else if (g_strcmp0 (help_command, "-v") == 0 || g_strcmp0 (help_command, "--version") == 0) { g_print ("%s v%s\n", PROJECT_NAME, PROJECT_VER); help_displayed = TRUE; } else if (g_strcmp0 (help_command, "--help-show") == 0 || g_strcmp0 (help_command, "help-show") == 0) { print_show_help (prg_name); help_displayed = TRUE; } else if (g_strcmp0 (help_command, "--help-export") == 0 || g_strcmp0 (help_command, "help-export") == 0) { print_export_help (prg_name); help_displayed = TRUE; } return help_displayed; } static void print_main_help (const gchar *prg_name) { GString *msg = g_string_new (_("Usage:\n %s
[option 1] [option 2] ...")); #if GLIB_CHECK_VERSION(2, 68, 0) g_string_replace (msg, "%s", prg_name, 0); #else g_string_replace_backported (msg, "%s", prg_name, 0); #endif g_print ("%s\n\n", msg->str); g_string_free (msg, TRUE); // Translators: please do not translate 'help' g_print ("%s\n", _("help command options:")); // Translators: please do not translate '-h, --help' g_print ("%s\n", _(" -h, --help\t\tShow this help")); // Translators: please do not translate '--help-show' g_print ("%s\n", _(" --help-show\t\tShow options")); // Translators: please do not translate '--help-export' g_print ("%s\n\n", _(" --help-export\t\tExport options")); g_print ("%s\n", _("Main options:")); // Translators: please do not translate '-v, --version' g_print ("%s\n", _(" -v, --version\t\t\t\tShow program version")); // Translators: please do not translate 'show <-a ..> [-i ..] [-m] [-n]' g_print ("%s\n", _(" show <-a ..> [-i ..] [-m] [-n]\tShow a token")); // Translators: please do not translate 'list' g_print ("%s\n", _(" list\t\t\t\t\tList all pairs of account and issuer")); // Translators: please do not translate 'export <-t ..> [-d ..]' g_print ("%s\n\n", _(" export <-t ..> [-d ..]\t\tExport data")); } static void print_show_help (const gchar *prg_name) { // Translators: please do not translate '%s show' GString *msg = g_string_new (_("Usage:\n %s show <-a ..> [-i ..] [-m]")); #if GLIB_CHECK_VERSION(2, 68, 0) g_string_replace (msg, "%s", prg_name, 0); #else g_string_replace_backported (msg, "%s", prg_name, 0); #endif g_print ("%s\n\n", msg->str); g_string_free (msg, TRUE); // Translators: please do not translate 'show' g_print ("%s\n", _("show command options:")); // Translators: please do not translate '-a, --account' g_print ("%s\n", _(" -a, --account\t\tThe account name (mandatory)")); // Translators: please do not translate '-i, --issuer' g_print ("%s\n", _(" -i, --issuer\t\tThe issuer name (optional)")); // Translators: please do not translate '-m, --match-exactly' g_print ("%s\n", _(" -m, --match-exactly\tShow the token only if it matches exactly the account and/or the issuer (optional)")); // Translators: please do not translate '-n, --next' g_print ("%s\n\n", _(" -n, --next\tShow also the next token, not only the current one (optional, valid only for TOTP)")); } static void print_export_help (const gchar *prg_name) { // Translators: please do not translate '%s export' GString *msg = g_string_new (_("Usage:\n %s export <-t> [-d ..]")); #if GLIB_CHECK_VERSION(2, 68, 0) g_string_replace (msg, "%s", prg_name, 0); #else g_string_replace_backported (msg, "%s", prg_name, 0); #endif g_print ("%s\n\n", msg->str); g_string_free (msg, TRUE); // Translators: please do not translate 'export' g_print ("%s\n", _("export command options:")); // Translators: please do not translate '-t, --type' g_print ("%s\n", _(" -t, --type\t\tExport format. Must be either one of: andotp_plain, andotp_encrypted, freeotpplus, aegis")); // Translators: please do not translate '-d, --directory' g_print ("%s\n", _(" -d, --directory\tThe output directory where the exported file will be saved.")); g_print ("%s\n\n", _("\t\t\tIf nothing is specified OR flatpak is being used, the output folder will be the user's HOME directory.")); } OTPClient-3.2.1/src/cli/help.h000066400000000000000000000002321452112020400157410ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS gboolean show_help (const gchar *prg_name, const gchar *help_command); G_END_DECLS OTPClient-3.2.1/src/cli/main.c000066400000000000000000000251221452112020400157350ustar00rootroot00000000000000#include #include #include #include #include #include #include "help.h" #include "get-data.h" #include "../common/common.h" #include "../common/exports.h" #include "../secret-schema.h" #define MAX_ABS_PATH_LEN 256 #ifndef USE_FLATPAK_APP_FOLDER static gchar *get_db_path (void); #endif static gchar *get_pwd (const gchar *pwd_msg); static gboolean get_use_secretservice (void); gint main (gint argc, gchar **argv) { if (show_help (argv[0], argv[1])) { return 0; } DatabaseData *db_data = g_new0 (DatabaseData, 1); db_data->key_stored = FALSE; db_data->max_file_size_from_memlock = get_max_file_size_from_memlock (); gchar *init_msg = init_libs (db_data->max_file_size_from_memlock); if (init_msg != NULL) { g_printerr ("%s\n", init_msg); g_free (init_msg); g_free (db_data); return -1; } #ifdef USE_FLATPAK_APP_FOLDER db_data->db_path = g_build_filename (g_get_user_data_dir (), "otpclient-db.enc", NULL); // on the first run the cfg file is not created in the flatpak version because we use a non-changeable db path gchar *cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); if (!g_file_test (cfg_file_path, G_FILE_TEST_EXISTS)) { g_file_set_contents (cfg_file_path, "[config]", -1, NULL); } g_free (cfg_file_path); #else db_data->db_path = get_db_path (); if (db_data->db_path == NULL) { g_free (db_data); return -1; } #endif gboolean use_secret_service = get_use_secretservice (); if (use_secret_service == TRUE) { gchar *pwd = secret_password_lookup_sync (OTPCLIENT_SCHEMA, NULL, NULL, "string", "main_pwd", NULL); if (pwd == NULL) { goto get_pwd; } else { db_data->key_stored = TRUE; db_data->key= secure_strdup (pwd); secret_password_free (pwd); } } else { get_pwd: db_data->key = get_pwd (_("Type the DB decryption password: ")); if (db_data->key == NULL) { g_free (db_data); return -1; } } db_data->objects_hash = NULL; GError *err = NULL; load_db (db_data, &err); if (err != NULL) { const gchar *tmp_msg = _("Error while loading the database:"); gchar *msg = g_strconcat (tmp_msg, " %s\n", err->message, NULL); g_printerr ("%s\n", msg); gcry_free (db_data->key); g_free (db_data); g_free (msg); return -1; } if (use_secret_service == TRUE && db_data->key_stored == FALSE) { secret_password_store (OTPCLIENT_SCHEMA, SECRET_COLLECTION_DEFAULT, "main_pwd", db_data->key, NULL, on_password_stored, NULL, "string", "main_pwd", NULL); } gchar *account = NULL, *issuer = NULL; gboolean show_next_token = FALSE, match_exactly = FALSE; if (g_strcmp0 (argv[1], "show") == 0) { if (argc < 4 || argc > 8) { // Translators: please do not translate '%s --help-show' g_printerr (_("Wrong argument(s). Please type '%s --help-show' to see the available options.\n"), argv[0]); g_free (db_data); return -1; } for (gint i = 2; i < argc; i++) { if (g_strcmp0 (argv[i], "-a") == 0) { account = argv[i + 1]; } else if (g_strcmp0 (argv[i], "-i") == 0) { issuer = argv[i + 1]; } else if (g_strcmp0 (argv[i], "-m") == 0) { match_exactly = TRUE; } else if (g_strcmp0 (argv[i], "-n") == 0) { show_next_token = TRUE; } } if (account == NULL) { // Translators: please do not translate 'account' g_printerr ("%s\n", _("[ERROR]: The account option (-a) must be specified and can not be empty.")); goto end; } show_token (db_data, account, issuer, match_exactly, show_next_token); } else if (g_strcmp0 (argv[1], "list") == 0) { list_all_acc_iss (db_data); } else if (g_strcmp0 (argv[1], "export") == 0) { if (g_ascii_strcasecmp (argv[3], "andotp_plain") != 0 && g_ascii_strcasecmp (argv[3], "andotp_encrypted") != 0 && g_ascii_strcasecmp (argv[3], "freeotpplus") != 0 && g_ascii_strcasecmp (argv[3], "aegis") != 0) { // Translators: please do not translate '%s --help-export' g_printerr (_("Wrong argument(s). Please type '%s --help-export' to see the available options.\n"), argv[0]); g_free (db_data); return -1; } const gchar *base_dir = NULL; #ifndef USE_FLATPAK_APP_FOLDER if (argv[4] == NULL) { base_dir = g_get_home_dir (); } else { if (g_ascii_strcasecmp (argv[4], "-d") == 0 && argv[5] != NULL) { if (!g_file_test (argv[5], G_FILE_TEST_IS_DIR)) { g_printerr (_("%s is not a directory or the folder doesn't exist. The output will be saved into the HOME directory.\n"), argv[5]); base_dir = g_get_home_dir (); } else { base_dir = argv[5]; } } else { g_printerr ("%s\n", _("Incorrect parameters used for setting the output folder. Therefore, the exported file will be saved into the HOME directory.")); base_dir = g_get_home_dir (); } } #else base_dir = g_get_user_data_dir (); #endif gchar *andotp_export_pwd = NULL, *exported_file_path = NULL, *ret_msg = NULL; if (g_ascii_strcasecmp (argv[3], "andotp_plain") == 0 || g_ascii_strcasecmp (argv[3], "andotp_encrypted") == 0) { if (g_ascii_strcasecmp (argv[3], "andotp_encrypted")) { andotp_export_pwd = get_pwd (_("Type the export encryption password: ")); if (andotp_export_pwd == NULL) { goto end; } } exported_file_path = g_build_filename (base_dir, andotp_export_pwd != NULL ? "andotp_exports.json.aes" : "andotp_exports.json", NULL); ret_msg = export_andotp (exported_file_path, andotp_export_pwd, db_data->json_data); gcry_free (andotp_export_pwd); } if (g_ascii_strcasecmp (argv[3], "freeotpplus") == 0) { exported_file_path = g_build_filename (base_dir, "freeotpplus-exports.txt", NULL); ret_msg = export_freeotpplus (exported_file_path, db_data->json_data); } if (g_ascii_strcasecmp (argv[3], "aegis") == 0) { exported_file_path = g_build_filename (base_dir, "aegis_export_plain.json", NULL); ret_msg = export_aegis (exported_file_path, db_data->json_data, NULL); } if (ret_msg != NULL) { g_printerr (_("An error occurred while exporting the data: %s\n"), ret_msg); g_free (ret_msg); } else { g_print (_("Data successfully exported to: %s\n"), exported_file_path); } g_free (exported_file_path); } else { show_help (argv[0], "help"); return -1; } end: gcry_free (db_data->key); g_free (db_data->db_path); g_slist_free_full (db_data->objects_hash, g_free); json_decref (db_data->json_data); g_free (db_data); return 0; } #ifndef USE_FLATPAK_APP_FOLDER static gchar * get_db_path (void) { gchar *db_path = NULL; GError *err = NULL; GKeyFile *kf = g_key_file_new (); gchar *cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); if (g_file_test (cfg_file_path, G_FILE_TEST_EXISTS)) { if (!g_key_file_load_from_file (kf, cfg_file_path, G_KEY_FILE_NONE, &err)) { g_printerr ("%s\n", err->message); g_key_file_free (kf); g_clear_error (&err); return NULL; } db_path = g_key_file_get_string (kf, "config", "db_path", NULL); if (db_path == NULL) { goto type_db_path; } if (!g_file_test (db_path, G_FILE_TEST_EXISTS)) { gchar *msg = g_strconcat ("Database file/location (", db_path, ") does not exist.\n", NULL); g_printerr ("%s\n", msg); g_free (msg); goto type_db_path; } goto end; } type_db_path: ; // empty statement workaround g_print ("%s", _("Type the absolute path to the database: ")); db_path = g_malloc0 (MAX_ABS_PATH_LEN); if (fgets (db_path, MAX_ABS_PATH_LEN, stdin) == NULL) { g_printerr ("%s\n", _("Couldn't get db path from stdin")); g_free (cfg_file_path); g_free (db_path); return NULL; } else { // remove the newline char db_path[g_utf8_strlen (db_path, -1) - 1] = '\0'; if (!g_file_test (db_path, G_FILE_TEST_EXISTS)) { g_printerr (_("File '%s' does not exist\n"), db_path); g_free (cfg_file_path); g_free (db_path); return NULL; } } end: g_free (cfg_file_path); return db_path; } #endif static gchar * get_pwd (const gchar *pwd_msg) { gchar *pwd = gcry_calloc_secure (256, 1); g_print ("%s", pwd_msg); struct termios old, new; if (tcgetattr (STDIN_FILENO, &old) != 0) { g_printerr ("%s\n", _("Couldn't get termios info")); gcry_free (pwd); return NULL; } new = old; new.c_lflag &= ~ECHO; if (tcsetattr (STDIN_FILENO, TCSAFLUSH, &new) != 0) { g_printerr ("%s\n", _("Couldn't turn echoing off")); gcry_free (pwd); return NULL; } if (fgets (pwd, 256, stdin) == NULL) { g_printerr ("%s\n", _("Couldn't read password from stdin")); gcry_free (pwd); return NULL; } g_print ("\n"); tcsetattr (STDIN_FILENO, TCSAFLUSH, &old); pwd[g_utf8_strlen (pwd, -1) - 1] = '\0'; gchar *realloc_pwd = gcry_realloc (pwd, g_utf8_strlen (pwd, -1) + 1); return realloc_pwd; } static gboolean get_use_secretservice (void) { gboolean use_secret_service = TRUE; // by default, we enable it GError *err = NULL; GKeyFile *kf = g_key_file_new (); gchar *cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); if (g_file_test (cfg_file_path, G_FILE_TEST_EXISTS)) { if (!g_key_file_load_from_file (kf, cfg_file_path, G_KEY_FILE_NONE, &err)) { g_printerr ("%s\n", err->message); g_key_file_free (kf); g_clear_error (&err); return FALSE; } use_secret_service = g_key_file_get_boolean (kf, "config", "use_secret_service", NULL); } return use_secret_service; } OTPClient-3.2.1/src/common/000077500000000000000000000000001452112020400153645ustar00rootroot00000000000000OTPClient-3.2.1/src/common/aegis.c000066400000000000000000000453251452112020400166310ustar00rootroot00000000000000#include #include #include #include #include #include "../imports.h" #include "../gquarks.h" #include "common.h" #define NONCE_SIZE 12 #define TAG_SIZE 16 #define SALT_SIZE 32 #define KEY_SIZE 32 static GSList *get_otps_from_plain_backup (const gchar *path, GError **err); static GSList *get_otps_from_encrypted_backup (const gchar *path, const gchar *password, gint32 max_file_size, GError **err); static GSList *parse_json_data (const gchar *data, GError **err); GSList * get_aegis_data (const gchar *path, const gchar *password, gint32 max_file_size, gboolean encrypted, GError **err) { if (g_file_test (path, G_FILE_TEST_IS_SYMLINK | G_FILE_TEST_IS_DIR) ) { g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "Selected file is either a symlink or a directory."); return NULL; } return (encrypted == TRUE) ? get_otps_from_encrypted_backup(path, password, max_file_size, err) : get_otps_from_plain_backup(path, err); } static GSList * get_otps_from_plain_backup (const gchar *path, GError **err) { json_error_t j_err; json_t *json = json_load_file (path, 0, &j_err); if (!json) { g_printerr ("Error loading json: %s\n", j_err.text); return NULL; } gchar *dumped_json = json_dumps(json_object_get (json, "db"), 0); GSList *otps = parse_json_data (dumped_json, err); gcry_free (dumped_json); return otps; } static GSList * get_otps_from_encrypted_backup (const gchar *path, const gchar *password, gint32 max_file_size, GError **err) { json_error_t j_err; json_t *json = json_load_file (path, 0, &j_err); if (!json) { g_printerr ("Error loading json: %s\n", j_err.text); return NULL; } json_t *arr = json_object_get (json_object_get(json, "header"), "slots"); gint index = 0; for (; index < json_array_size(arr); index++) { json_t *j_type = json_object_get (json_array_get(arr, index), "type"); json_int_t int_type = json_integer_value (j_type); if (int_type == 1) break; } json_t *wanted_obj = json_array_get (arr, index); gint n = (gint)json_integer_value (json_object_get (wanted_obj, "n")); gint p = (gint)json_integer_value (json_object_get (wanted_obj, "p")); guchar *salt = hexstr_to_bytes (json_string_value (json_object_get (wanted_obj, "salt"))); guchar *enc_key = hexstr_to_bytes(json_string_value (json_object_get (wanted_obj, "key"))); json_t *kp = json_object_get (wanted_obj, "key_params"); guchar *key_nonce = hexstr_to_bytes (json_string_value (json_object_get (kp, "nonce"))); guchar *key_tag = hexstr_to_bytes (json_string_value (json_object_get (kp, "tag"))); json_t *dbp = json_object_get(json_object_get(json, "header"), "params"); guchar *keybuf = gcry_malloc (KEY_SIZE); if (gcry_kdf_derive (password, g_utf8_strlen (password, -1), GCRY_KDF_SCRYPT, n, salt, SALT_SIZE, p, KEY_SIZE, keybuf) != 0) { g_printerr ("Error while deriving the key.\n"); g_free (salt); g_free (enc_key); g_free (key_nonce); g_free (key_tag); gcry_free (keybuf); json_decref (json); return NULL; } gcry_cipher_hd_t hd = open_cipher_and_set_data (keybuf, key_nonce, NONCE_SIZE); if (hd == NULL) { g_free (salt); g_free (enc_key); g_free (key_nonce); g_free (key_tag); gcry_free (keybuf); json_decref (json); return NULL; } guchar *master_key = gcry_calloc_secure (KEY_SIZE, 1); if (gcry_cipher_decrypt (hd, master_key, KEY_SIZE, enc_key, KEY_SIZE) != 0) { g_printerr ("Error while decrypting the master key.\n"); g_free (salt); g_free (enc_key); g_free (key_nonce); g_free (key_tag); gcry_free (master_key); gcry_free (keybuf); gcry_cipher_close (hd); json_decref (json); return NULL; } gpg_error_t gpg_err = gcry_cipher_checktag (hd, key_tag, TAG_SIZE); if (gpg_err != 0) { g_set_error (err, bad_tag_gquark (), BAD_TAG_ERRCODE, "Invalid TAG (master key). Either the password is wrong or the file is corrupted."); g_free (salt); g_free (enc_key); g_free (key_nonce); g_free (key_tag); gcry_free (master_key); gcry_free (keybuf); gcry_cipher_close (hd); json_decref (json); return NULL; } g_free (salt); g_free (enc_key); g_free (key_nonce); g_free (key_tag); gcry_free (keybuf); gcry_cipher_close (hd); guchar *nonce = hexstr_to_bytes (json_string_value (json_object_get (dbp, "nonce"))); guchar *tag = hexstr_to_bytes (json_string_value (json_object_get (dbp, "tag"))); hd = open_cipher_and_set_data (master_key, nonce, 12); if (hd == NULL) { g_free (tag); g_free (nonce); gcry_free (master_key); json_decref (json); return NULL; } gsize out_len; guchar *b64decoded_db = g_base64_decode (json_string_value (json_object_get (json, "db")), &out_len); if (out_len > max_file_size) { g_set_error (err, file_too_big_gquark (), FILE_TOO_BIG, "File is too big"); g_free (tag); g_free (nonce); gcry_free (master_key); g_free (b64decoded_db); gcry_cipher_close (hd); json_decref (json); return NULL; } // we no longer need the json object, so we can free up some secure memory json_decref (json); gchar *decrypted_db = gcry_calloc_secure (out_len, 1); gpg_err = gcry_cipher_decrypt (hd, decrypted_db, out_len, b64decoded_db, out_len); if (gpg_err) { goto clean_and_exit; } gpg_err = gcry_cipher_checktag (hd, tag, TAG_SIZE); if (gpg_err != 0) { g_set_error (err, bad_tag_gquark (), BAD_TAG_ERRCODE, "Invalid TAG (database). Either the password is wrong or the file is corrupted."); clean_and_exit: g_free (b64decoded_db); g_free (nonce); g_free (tag); gcry_free (master_key); gcry_free (decrypted_db); gcry_cipher_close (hd); return NULL; } g_free (b64decoded_db); g_free (nonce); g_free (tag); gcry_cipher_close (hd); gcry_free (master_key); // we remove the icon field (and the icon_mime while at it too) because it uses lots of secure memory for nothing GRegex *regex = g_regex_new (".*\"icon\":(\\s)*\".*\",\\n|.*\"icon_mime\":(\\s)*\".*\",\\n", G_REGEX_MULTILINE, 0, NULL); gchar *cleaned_db = secure_strdup (g_regex_replace (regex, decrypted_db, -1, 0, "", 0, NULL)); g_regex_unref (regex); gcry_free (decrypted_db); GSList *otps = parse_json_data (cleaned_db, err); gcry_free (cleaned_db); return otps; } gchar * export_aegis (const gchar *export_path, json_t *json_db_data, const gchar *password) { GError *err = NULL; json_t *root = json_object (); json_object_set (root, "version", json_integer (1)); gcry_cipher_hd_t hd; guchar *derived_master_key = NULL, *enc_master_key = NULL, *key_nonce = NULL, *key_tag = NULL, *db_nonce = NULL, *db_tag = NULL, *salt = NULL; json_t *aegis_header_obj = json_object (); if (password == NULL) { json_object_set (aegis_header_obj, "slots", json_null ()); json_object_set (aegis_header_obj, "params", json_null ()); } else { json_t *slots_arr = json_array(); json_t *slot_1 = json_object(); json_array_append (slots_arr, slot_1); json_object_set (slot_1, "type", json_integer (1)); uuid_t binuuid; uuid_generate_random (binuuid); gchar *uuid = g_malloc0 (37); uuid_unparse_lower (binuuid, uuid); json_object_set (slot_1, "uuid", json_string (g_strdup (uuid))); g_free (uuid); salt = g_malloc0 (SALT_SIZE); gcry_create_nonce (salt, SALT_SIZE); key_nonce = g_malloc0 (NONCE_SIZE); gcry_create_nonce (key_nonce, NONCE_SIZE); derived_master_key = gcry_calloc_secure(KEY_SIZE, 1); gpg_error_t gpg_err = gcry_kdf_derive (password, g_utf8_strlen (password, -1), GCRY_KDF_SCRYPT, 32768, salt, SALT_SIZE, 1, KEY_SIZE, derived_master_key); if (gpg_err) { g_printerr ("Error while deriving the key\n"); gcry_free (derived_master_key); return NULL; } hd = open_cipher_and_set_data (derived_master_key, key_nonce, NONCE_SIZE); if (hd == NULL) { gcry_free (derived_master_key); g_free (key_nonce); g_free (salt); return NULL; } enc_master_key = gcry_malloc (KEY_SIZE); if (gcry_cipher_encrypt (hd, enc_master_key, KEY_SIZE, derived_master_key, KEY_SIZE)) { g_printerr ("Error while encrypting the master key.\n"); gcry_free (derived_master_key); gcry_free (enc_master_key); g_free (key_nonce); g_free (salt); gcry_cipher_close (hd); return NULL; } key_tag = g_malloc0 (TAG_SIZE); gcry_cipher_gettag (hd, key_tag, TAG_SIZE); json_object_set (slot_1, "key", json_string (bytes_to_hexstr (enc_master_key, KEY_SIZE))); gcry_cipher_close (hd); json_t *kp = json_object(); json_object_set (kp, "nonce", json_string(bytes_to_hexstr (key_nonce, NONCE_SIZE))); json_object_set (kp, "tag", json_string (bytes_to_hexstr (key_tag, TAG_SIZE))); json_object_set (slot_1, "key_params", kp); json_object_set (slot_1, "n", json_integer (32768)); json_object_set (slot_1, "r", json_integer (8)); json_object_set (slot_1, "p", json_integer (1)); json_object_set (slot_1, "salt", json_string (bytes_to_hexstr (salt, SALT_SIZE))); json_object_set (aegis_header_obj, "slots", slots_arr); json_t *db_params_obj = json_object(); db_nonce = g_malloc0 (NONCE_SIZE); gcry_create_nonce (db_nonce, NONCE_SIZE); json_object_set (db_params_obj, "nonce", json_string (bytes_to_hexstr (db_nonce, NONCE_SIZE))); db_tag = g_malloc0 (TAG_SIZE); // tag is computed after encryption, so we just put a placeholder here json_object_set (db_params_obj, "tag", json_null ()); json_object_set (aegis_header_obj, "params", db_params_obj); } json_object_set (root, "header", aegis_header_obj); json_t *aegis_db_obj = json_object (); json_t *array = json_array (); json_object_set (aegis_db_obj, "version", json_integer (2)); json_object_set (aegis_db_obj, "entries", array); json_object_set (root, "db", aegis_db_obj); json_t *db_obj, *export_obj, *info_obj; gsize index; json_array_foreach (json_db_data, index, db_obj) { export_obj = json_object (); info_obj = json_object (); json_t *otp_type = json_object_get (db_obj, "type"); const gchar *issuer = json_string_value (json_object_get (db_obj, "issuer")); if (issuer != NULL && g_ascii_strcasecmp (issuer, "steam") == 0) { json_object_set (export_obj, "type", json_string ("steam")); } else { json_object_set (export_obj, "type", json_string (g_utf8_strdown (json_string_value (otp_type), -1))); } json_object_set (export_obj, "name", json_object_get (db_obj, "label")); const gchar *issuer_from_db = json_string_value (json_object_get (db_obj, "issuer")); if (issuer_from_db != NULL && g_utf8_strlen (issuer_from_db, -1) > 0) { json_object_set (export_obj, "issuer", json_string (issuer_from_db)); } else { json_object_set (export_obj, "issuer", json_null ()); } json_object_set (export_obj, "icon", json_null ()); json_object_set (info_obj, "secret", json_object_get (db_obj, "secret")); json_object_set (info_obj, "digits", json_object_get (db_obj, "digits")); json_object_set (info_obj, "algo", json_object_get (db_obj, "algo")); if (g_ascii_strcasecmp (json_string_value (otp_type), "TOTP") == 0) { json_object_set (info_obj, "period", json_object_get (db_obj, "period")); } else { json_object_set (info_obj, "counter", json_object_get (db_obj, "counter")); } json_object_set (export_obj, "info", info_obj); json_array_append (array, export_obj); } if (password != NULL) { hd = open_cipher_and_set_data (derived_master_key, db_nonce, NONCE_SIZE); if (hd == NULL) { goto clean_and_return; } size_t db_size = json_dumpb (aegis_db_obj, NULL, 0, 0); guchar *enc_db = g_malloc0 (db_size); gchar *dumped_db = g_malloc0 (db_size); json_dumpb (aegis_db_obj, dumped_db, db_size, 0); if (gcry_cipher_encrypt (hd, enc_db, db_size, dumped_db, db_size)) { g_printerr ("Error while encrypting the db.\n"); g_free (enc_db); g_free (dumped_db); gcry_cipher_close (hd); clean_and_return: g_free (key_nonce); g_free (key_tag); g_free (db_nonce); g_free (db_tag); g_free (salt); gcry_free (derived_master_key); gcry_free (enc_master_key); return NULL; } gcry_cipher_gettag (hd, db_tag, TAG_SIZE); json_t *db_params = json_object_get (aegis_header_obj, "params"); json_object_set (db_params, "tag", json_string (bytes_to_hexstr (db_tag, TAG_SIZE))); g_free (dumped_db); gchar *b64enc_db = g_base64_encode (enc_db, db_size); json_object_set (root, "db", json_string (b64enc_db)); g_free (b64enc_db); g_free (enc_db); g_free (key_nonce); g_free (key_tag); g_free (db_nonce); g_free (db_tag); g_free (salt); gcry_free (derived_master_key); gcry_free (enc_master_key); gcry_cipher_close (hd); } GFile *out_gfile = g_file_new_for_path (export_path); GFileOutputStream *out_stream = g_file_replace (out_gfile, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION | G_FILE_CREATE_PRIVATE, NULL, &err); if (err == NULL) { gsize jbuf_size = json_dumpb (root, NULL, 0, 0); if (jbuf_size == 0) { goto cleanup_and_exit; } gchar *jbuf = g_malloc0 (jbuf_size); if (json_dumpb (root, jbuf, jbuf_size, JSON_COMPACT) == -1) { g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "couldn't dump json data to buffer"); g_free (jbuf); goto cleanup_and_exit; } if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), jbuf, jbuf_size, NULL, &err) == -1) { g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "couldn't dump json data to file"); g_free (jbuf); goto cleanup_and_exit; } g_free (jbuf); } else { g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "couldn't create the file object"); } cleanup_and_exit: json_array_clear (array); json_decref (aegis_db_obj); json_decref (aegis_header_obj); json_decref (root); g_object_unref (out_stream); g_object_unref (out_gfile); return (err != NULL ? g_strdup (err->message) : NULL); } static GSList * parse_json_data (const gchar *data, GError **err) { json_error_t jerr; json_t *root = json_loads (data, JSON_DISABLE_EOF_CHECK, &jerr); if (root == NULL) { g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "%s", jerr.text); return NULL; } json_t *array = json_object_get (root, "entries"); if (array == NULL) { g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "%s", jerr.text); json_decref (root); return NULL; } GSList *otps = NULL; for (guint i = 0; i < json_array_size (array); i++) { json_t *obj = json_array_get (array, i); otp_t *otp = g_new0 (otp_t, 1); otp->issuer = g_strdup (json_string_value (json_object_get (obj, "issuer"))); otp->account_name = g_strdup (json_string_value (json_object_get (obj, "name"))); json_t *info_obj = json_object_get (obj, "info"); otp->secret = secure_strdup (json_string_value (json_object_get (info_obj, "secret"))); otp->digits = (guint32) json_integer_value (json_object_get(info_obj, "digits")); const gchar *type = json_string_value (json_object_get (obj, "type")); if (g_ascii_strcasecmp (type, "TOTP") == 0) { otp->type = g_strdup (type); otp->period = (guint32)json_integer_value (json_object_get (info_obj, "period")); } else if (g_ascii_strcasecmp (type, "HOTP") == 0) { otp->type = g_strdup (type); otp->counter = json_integer_value (json_object_get (info_obj, "counter")); } else if (g_ascii_strcasecmp (type, "Steam") == 0) { otp->type = g_strdup ("TOTP"); otp->period = (guint32)json_integer_value (json_object_get (info_obj, "period")); if (otp->period == 0) { // Aegis exported backup for Steam might not contain the period field, otp->period = 30; } g_free (otp->issuer); otp->issuer = g_strdup ("Steam"); } else { g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "otp type is neither TOTP nor HOTP"); gcry_free (otp->secret); g_free (otp); json_decref (obj); return NULL; } const gchar *algo = json_string_value (json_object_get (info_obj, "algo")); if (g_ascii_strcasecmp (algo, "SHA1") == 0 || g_ascii_strcasecmp (algo, "SHA256") == 0 || g_ascii_strcasecmp (algo, "SHA512") == 0) { otp->algo = g_ascii_strup (algo, -1); } else { g_printerr ("algo not supported (must be either one of: sha1, sha256 or sha512\n"); gcry_free (otp->secret); g_free (otp); json_decref (obj); json_decref (info_obj); return NULL; } otps = g_slist_append (otps, g_memdupX (otp, sizeof (otp_t))); g_free (otp); } json_decref (root); return otps; } OTPClient-3.2.1/src/common/andotp.c000066400000000000000000000404041452112020400170170ustar00rootroot00000000000000#include #include #include #include #include #include #include "../file-size.h" #include "../imports.h" #include "../gquarks.h" #include "common.h" #define ANDOTP_IV_SIZE 12 #define ANDOTP_SALT_SIZE 12 #define ANDOTP_TAG_SIZE 16 #define PBKDF2_MIN_BACKUP_ITERATIONS 140000 #define PBKDF2_MAX_BACKUP_ITERATIONS 160000 static GSList *get_otps_from_encrypted_backup (const gchar *path, const gchar *password, gint32 max_file_size, GFile *in_file, GFileInputStream *in_stream, GError **err); static GSList *get_otps_from_plain_backup (const gchar *path, GError **err); static guchar *get_derived_key (const gchar *password, const guchar *salt, guint32 iterations); static GSList *parse_json_data (const gchar *data, GError **err); GSList * get_andotp_data (const gchar *path, const gchar *password, gint32 max_file_size, gboolean encrypted, GError **err) { GFile *in_file = g_file_new_for_path(path); GFileInputStream *in_stream = g_file_read(in_file, NULL, err); if (*err != NULL) { g_object_unref(in_file); return NULL; } return (encrypted == TRUE) ? get_otps_from_encrypted_backup (path, password, max_file_size, in_file, in_stream, err) : get_otps_from_plain_backup (path, err); } static GSList * get_otps_from_encrypted_backup (const gchar *path, const gchar *password, gint32 max_file_size, GFile *in_file, GFileInputStream *in_stream, GError **err) { gint32 le_iterations; if (g_input_stream_read (G_INPUT_STREAM (in_stream), &le_iterations, 4, NULL, err) == -1) { g_object_unref (in_stream); g_object_unref (in_file); return NULL; } guint32 be_iterations = __builtin_bswap32 (le_iterations); if (be_iterations < PBKDF2_MIN_BACKUP_ITERATIONS || be_iterations > PBKDF2_MAX_BACKUP_ITERATIONS) { // https://github.com/andOTP/andOTP/blob/6c54b8811f950375c774b2eefebcf1f9fa13d433/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java#L124-L125 g_object_unref (in_stream); g_object_unref (in_file); g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "Number of iterations is invalid. It's likely this is not an andOTP encrypted database.\n"); return NULL; } guchar salt[ANDOTP_SALT_SIZE]; if (g_input_stream_read (G_INPUT_STREAM (in_stream), salt, ANDOTP_SALT_SIZE, NULL, err) == -1) { g_object_unref (in_stream); g_object_unref (in_file); return NULL; } guchar iv[ANDOTP_IV_SIZE]; if (g_input_stream_read (G_INPUT_STREAM (in_stream), iv, ANDOTP_IV_SIZE, NULL, err) == -1) { g_object_unref (in_stream); g_object_unref (in_file); return NULL; } goffset input_file_size = get_file_size (path); guchar tag[ANDOTP_TAG_SIZE]; if (!g_seekable_seek (G_SEEKABLE (in_stream), input_file_size - ANDOTP_TAG_SIZE, G_SEEK_SET, NULL, err)) { g_object_unref (in_stream); g_object_unref (in_file); return NULL; } if (g_input_stream_read (G_INPUT_STREAM (in_stream), tag, ANDOTP_TAG_SIZE, NULL, err) == -1) { g_object_unref (in_stream); g_object_unref (in_file); return NULL; } // 4 is the size of iterations (int32) gsize enc_buf_size = (gsize) input_file_size - 4 - ANDOTP_SALT_SIZE - ANDOTP_IV_SIZE - ANDOTP_TAG_SIZE; if (enc_buf_size < 1) { g_printerr ("A non-encrypted file has been selected\n"); g_object_unref (in_stream); g_object_unref (in_file); return NULL; } else if (enc_buf_size > max_file_size) { g_object_unref (in_stream); g_object_unref (in_file); g_set_error (err, file_too_big_gquark (), FILE_TOO_BIG, "File is too big"); return NULL; } guchar *enc_buf = g_malloc0 (enc_buf_size); if (!g_seekable_seek (G_SEEKABLE (in_stream), 4 + ANDOTP_SALT_SIZE + ANDOTP_IV_SIZE, G_SEEK_SET, NULL, err)) { g_object_unref (in_stream); g_object_unref (in_file); g_free (enc_buf); return NULL; } if (g_input_stream_read (G_INPUT_STREAM (in_stream), enc_buf, enc_buf_size, NULL, err) == -1) { g_object_unref (in_stream); g_object_unref (in_file); g_free (enc_buf); return NULL; } g_object_unref (in_stream); g_object_unref (in_file); guchar *derived_key = get_derived_key (password, salt, be_iterations); gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, ANDOTP_IV_SIZE); if (hd == NULL) { gcry_free (derived_key); g_free (enc_buf); return NULL; } gchar *decrypted_json = gcry_calloc_secure (enc_buf_size, 1); gpg_error_t gpg_err = gcry_cipher_decrypt (hd, decrypted_json, enc_buf_size, enc_buf, enc_buf_size); if (gpg_err) { g_free (enc_buf); gcry_free (derived_key); gcry_free (decrypted_json); gcry_cipher_close (hd); return NULL; } if (gcry_err_code (gcry_cipher_checktag (hd, tag, ANDOTP_TAG_SIZE)) == GPG_ERR_CHECKSUM) { g_set_error (err, bad_tag_gquark (), BAD_TAG_ERRCODE, "Either the file is corrupted or the password is wrong"); gcry_cipher_close (hd); g_free (enc_buf); gcry_free (derived_key); gcry_free (decrypted_json); return NULL; } gcry_cipher_close (hd); gcry_free (derived_key); g_free (enc_buf); GSList *otps = parse_json_data (decrypted_json, err); gcry_free (decrypted_json); return otps; } static GSList * get_otps_from_plain_backup (const gchar *path, GError **err) { gchar *plain_json_data; gsize read_len; if (!g_file_get_contents (path, &plain_json_data, &read_len, err)) { return NULL; } GSList *otps = parse_json_data (plain_json_data, err); g_free (plain_json_data); return otps; } gchar * export_andotp (const gchar *export_path, const gchar *password, json_t *json_db_data) { GError *err = NULL; json_t *array = json_array (); json_t *db_obj, *export_obj; gsize index; json_array_foreach (json_db_data, index, db_obj) { export_obj = json_object (); const gchar *issuer = json_string_value (json_object_get (db_obj, "issuer")); if (issuer != NULL && g_ascii_strcasecmp (issuer, "steam") == 0) { json_object_set (export_obj, "type", json_string ("STEAM")); } else { json_object_set (export_obj, "type", json_object_get (db_obj, "type")); } json_object_set(export_obj, "issuer", json_object_get (db_obj, "issuer")); json_object_set (export_obj, "label", json_object_get (db_obj, "label")); json_object_set (export_obj, "secret", json_object_get (db_obj, "secret")); json_object_set (export_obj, "digits", json_object_get (db_obj, "digits")); json_object_set (export_obj, "algorithm", json_object_get (db_obj, "algo")); if (g_ascii_strcasecmp (json_string_value (json_object_get (db_obj, "type")), "TOTP") == 0) { json_object_set (export_obj, "period", json_object_get (db_obj, "period")); } else { json_object_set (export_obj, "counter", json_object_get (db_obj, "counter")); } json_array_append (array, export_obj); } // if plaintext export is needed, then write the file and exit if (password == NULL) { GFile *out_gfile = g_file_new_for_path (export_path); GFileOutputStream *out_stream = g_file_replace (out_gfile, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION | G_FILE_CREATE_PRIVATE, NULL, &err); if (err == NULL) { gsize jbuf_size = json_dumpb (array, NULL, 0, 0); if (jbuf_size == 0) { g_object_unref (out_stream); g_object_unref (out_gfile); goto end; } gchar *jbuf = g_malloc0 (jbuf_size); if (json_dumpb (array, jbuf, jbuf_size, JSON_COMPACT) == -1) { g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "couldn't dump json data to buffer"); g_free (jbuf); g_object_unref (out_stream); g_object_unref (out_gfile); goto end; } if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), jbuf, jbuf_size, NULL, &err) == -1) { g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "couldn't dump json data to file"); } g_free (jbuf); g_object_unref (out_stream); g_object_unref (out_gfile); } else { g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "couldn't create the file object"); g_object_unref (out_gfile); } goto end; } gchar *json_data = json_dumps (array, JSON_COMPACT); if (json_data == NULL) { json_array_clear (array); g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "couldn't dump json data"); goto end; } gsize json_data_size = strlen (json_data); // https://github.com/andOTP/andOTP/blob/bb01bbd242ace1a2e2620263d950d9852772f051/app/src/main/java/org/shadowice/flocke/andotp/Utilities/Constants.java#L109-L110 guint32 le_iterations = (g_random_int () % (PBKDF2_MAX_BACKUP_ITERATIONS - PBKDF2_MIN_BACKUP_ITERATIONS + 1)) + PBKDF2_MIN_BACKUP_ITERATIONS; gint32 be_iterations = (gint32)__builtin_bswap32 (le_iterations); guchar *iv = g_malloc0 (ANDOTP_IV_SIZE); gcry_create_nonce (iv, ANDOTP_IV_SIZE); guchar *salt = g_malloc0 (ANDOTP_SALT_SIZE); gcry_create_nonce (salt, ANDOTP_SALT_SIZE); guchar *derived_key = get_derived_key (password, salt, le_iterations); gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, iv, ANDOTP_IV_SIZE); if (hd == NULL) { gcry_free (derived_key); g_free (iv); g_free (salt); return NULL; } gchar *enc_buf = gcry_calloc_secure (json_data_size, 1); gpg_error_t gpg_err = gcry_cipher_encrypt (hd, enc_buf, json_data_size, json_data, json_data_size); if (gpg_err) { g_printerr ("%s\n", _("Error while encrypting the data.")); gcry_free (derived_key); gcry_free (enc_buf); g_free (iv); g_free (salt); gcry_cipher_close (hd); return NULL; } guchar tag[ANDOTP_TAG_SIZE]; gcry_cipher_gettag (hd, tag, ANDOTP_TAG_SIZE); gcry_cipher_close (hd); GFile *out_gfile = g_file_new_for_path (export_path); GFileOutputStream *out_stream = g_file_replace (out_gfile, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION | G_FILE_CREATE_PRIVATE, NULL, &err); if (err != NULL) { goto cleanup_before_exiting; } if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), &be_iterations, 4, NULL, &err) == -1) { goto cleanup_before_exiting; } if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), salt, ANDOTP_SALT_SIZE, NULL, &err) == -1) { goto cleanup_before_exiting; } if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), iv, ANDOTP_IV_SIZE, NULL, &err) == -1) { goto cleanup_before_exiting; } if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), enc_buf, json_data_size, NULL, &err) == -1) { goto cleanup_before_exiting; } if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), tag, ANDOTP_TAG_SIZE, NULL, &err) == -1) { goto cleanup_before_exiting; } cleanup_before_exiting: g_free (iv); g_free (salt); gcry_free (json_data); gcry_free (derived_key); gcry_free (enc_buf); json_array_clear (array); g_object_unref (out_stream); g_object_unref (out_gfile); end: return (err != NULL ? g_strdup (err->message) : NULL); } static guchar * get_derived_key (const gchar *password, const guchar *salt, guint32 iterations) { guchar *derived_key = gcry_malloc_secure (32); if (gcry_kdf_derive (password, (gsize) g_utf8_strlen (password, -1), GCRY_KDF_PBKDF2, GCRY_MD_SHA1, salt, ANDOTP_SALT_SIZE, iterations, 32, derived_key) != 0) { gcry_free (derived_key); return NULL; } return derived_key; } static GSList * parse_json_data (const gchar *data, GError **err) { json_error_t jerr; json_t *array = json_loads (data, JSON_DISABLE_EOF_CHECK, &jerr); if (array == NULL) { g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "%s", jerr.text); return NULL; } GSList *otps = NULL; for (guint i = 0; i < json_array_size (array); i++) { json_t *obj = json_array_get (array, i); otp_t *otp = g_new0 (otp_t, 1); otp->secret = secure_strdup (json_string_value (json_object_get (obj, "secret"))); const gchar *issuer = json_string_value (json_object_get (obj, "issuer")); if (issuer != NULL && g_utf8_strlen (issuer, -1) > 1) { otp->issuer = g_strstrip (g_strdup (issuer)); } const gchar *label_with_prefix = json_string_value (json_object_get (obj, "label")); gchar **tokens = g_strsplit (label_with_prefix, ":", -1); if (tokens[0] && tokens[1]) { if (issuer != NULL && g_ascii_strcasecmp(issuer, tokens[0]) == 0) { otp->account_name = g_strstrip (g_strdup (tokens[1])); } else { otp->issuer = g_strstrip (g_strdup (tokens[0])); otp->account_name = g_strstrip (g_strdup (tokens[1])); } } else { otp->account_name = g_strstrip (g_strdup (tokens[0])); } g_strfreev (tokens); otp->digits = (guint32) json_integer_value (json_object_get(obj, "digits")); const gchar *type = json_string_value (json_object_get (obj, "type")); if (g_ascii_strcasecmp (type, "TOTP") == 0) { otp->type = g_strdup (type); otp->period = (guint32)json_integer_value (json_object_get (obj, "period")); } else if (g_ascii_strcasecmp (type, "HOTP") == 0) { otp->type = g_strdup (type); otp->counter = json_integer_value (json_object_get (obj, "counter")); } else if (g_ascii_strcasecmp (type, "Steam") == 0) { otp->type = g_strdup ("TOTP"); otp->period = (guint32)json_integer_value (json_object_get (obj, "period")); if (otp->period == 0) { // andOTP exported backup for Steam might not contain the period field, otp->period = 30; } g_free (otp->issuer); otp->issuer = g_strdup ("Steam"); } else { g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "otp type is neither TOTP nor HOTP"); gcry_free (otp->secret); g_free (otp); json_decref (obj); return NULL; } const gchar *algo = json_string_value (json_object_get (obj, "algorithm")); if (g_ascii_strcasecmp (algo, "SHA1") == 0 || g_ascii_strcasecmp (algo, "SHA256") == 0 || g_ascii_strcasecmp (algo, "SHA512") == 0) { otp->algo = g_ascii_strup (algo, -1); } else { g_printerr ("algo not supported (must be either one of: sha1, sha256 or sha512\n"); gcry_free (otp->secret); g_free (otp); json_decref (obj); return NULL; } otps = g_slist_append (otps, g_memdupX (otp, sizeof (otp_t))); g_free (otp); } json_decref (array); return otps; } OTPClient-3.2.1/src/common/common.c000066400000000000000000000312521452112020400170230ustar00rootroot00000000000000#include #include #include #include #include "gcrypt.h" #include "jansson.h" #include "common.h" #include "../google-migration.pb-c.h" gint32 get_max_file_size_from_memlock (void) { const gchar *link = "https://github.com/paolostivanin/OTPClient/wiki/Secure-Memory-Limitations"; struct rlimit r; if (getrlimit (RLIMIT_MEMLOCK, &r) == -1) { // couldn't get memlock limit, so falling back to a default, low value g_print ("[WARNING] your OS's memlock limit may be too low for you (64000 bytes). Please have a look at %s\n", link); return LOW_MEMLOCK_VALUE; } else { if (r.rlim_cur == -1 || r.rlim_cur > MEMLOCK_VALUE) { // memlock is either unlimited or bigger than needed, so defaulting to 'MEMLOCK_VALUE' return MEMLOCK_VALUE; } else { // memlock is less than 'MEMLOCK_VALUE' g_print ("[WARNING] your OS's memlock limit may be too low for you (current value: %d bytes).\n" "This may cause issues when importing third parties databases or dealing with tens of tokens.\n" "For information on how to increase the memlock value, please have a look at %s\n", (gint32)r.rlim_cur, link); return (gint32)r.rlim_cur; } } } gchar * init_libs (gint32 max_file_size) { if (!gcry_check_version ("1.8.0")) { return g_strdup ("The required version of GCrypt is 1.8.0 or greater."); } if (gcry_control (GCRYCTL_INIT_SECMEM, max_file_size, 0)) { return g_strdup ("Couldn't initialize secure memory.\n"); } gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); json_set_alloc_funcs (gcry_malloc_secure, gcry_free); return NULL; } gint get_algo_int_from_str (const gchar *algo) { gint algo_int; if (g_strcmp0 (algo, "SHA1") == 0) { algo_int = SHA1; } else if (g_strcmp0 (algo, "SHA256") == 0) { algo_int = SHA256; } else { algo_int = SHA512; } return algo_int; } guint32 jenkins_one_at_a_time_hash (const gchar *key, gsize len) { guint32 hash, i; for (hash = i = 0; i < len; ++i) { hash += key[i]; hash += (hash << 10); hash ^= (hash >> 6); } hash += (hash << 3); hash ^= (hash >> 11); hash += (hash << 15); return hash; } guint32 json_object_get_hash (json_t *obj) { const gchar *key; json_t *value; gchar *tmp_string = gcry_calloc_secure (256, 1); json_object_foreach (obj, key, value) { if (g_strcmp0 (key, "period") == 0 || g_strcmp0 (key, "counter") == 0 || g_strcmp0 (key, "digits") == 0) { json_int_t v = json_integer_value (value); g_snprintf (tmp_string + g_utf8_strlen (tmp_string, -1), 256, "%ld", (gint64) v); } else { if (g_strlcat (tmp_string, json_string_value (value), 256) > 256) { g_printerr ("%s\n", _("Truncation occurred.")); } } } guint32 hash = jenkins_one_at_a_time_hash (tmp_string, strlen (tmp_string) + 1); gcry_free (tmp_string); return hash; } gchar * secure_strdup (const gchar *src) { gchar *sec_buf = gcry_calloc_secure (strlen (src) + 1, 1); memcpy (sec_buf, src, strlen (src) + 1); return sec_buf; } gchar * g_trim_whitespace (const gchar *str) { if (g_utf8_strlen (str, -1) == 0) { return NULL; } gchar *sec_buf = gcry_calloc_secure (strlen (str) + 1, 1); int pos = 0; for (int i = 0; str[i]; i++) { if (str[i] != ' ') { sec_buf[pos++] = str[i]; } } sec_buf[pos] = '\0'; gchar *secubf_newpos = (gchar *)gcry_realloc (sec_buf, strlen (sec_buf) + 1); return secubf_newpos; } guchar * hexstr_to_bytes (const gchar *hexstr) { size_t len = g_utf8_strlen (hexstr, -1); size_t final_len = len / 2; guchar *chrs = (guchar *)g_malloc ((final_len+1) * sizeof(*chrs)); for (size_t i = 0, j = 0; j < final_len; i += 2, j++) chrs[j] = (hexstr[i] % 32 + 9) % 25 * 16 + (hexstr[i+1] % 32 + 9) % 25; chrs[final_len] = '\0'; return chrs; } gchar * bytes_to_hexstr (const guchar *data, size_t datalen) { gchar hex_str[]= "0123456789abcdef"; gchar *result = g_malloc0(datalen * 2 + 1); if (result == NULL) { g_printerr ("Error while allocating memory for bytes_to_hexstr.\n"); return result; } for (guint i = 0; i < datalen; i++) { result[i * 2 + 0] = hex_str[(data[i] >> 4) & 0x0F]; result[i * 2 + 1] = hex_str[(data[i] ) & 0x0F]; } result[datalen * 2] = 0; return result; } // Backported from Glib 2.68 in order to support Debian "bullseye" and Ubuntu 20.04 guint g_string_replace_backported (GString *string, const gchar *find, const gchar *replace, guint limit) { gsize f_len, r_len, pos; gchar *cur, *next; guint n = 0; g_return_val_if_fail (string != NULL, 0); g_return_val_if_fail (find != NULL, 0); g_return_val_if_fail (replace != NULL, 0); f_len = g_utf8_strlen (find, -1); r_len = g_utf8_strlen (replace, -1); cur = string->str; while ((next = strstr (cur, find)) != NULL) { pos = next - string->str; g_string_erase (string, (gssize)pos, (gssize)f_len); g_string_insert (string, (gssize)pos, replace); cur = string->str + pos + r_len; n++; /* Only match the empty string once at any given position, to * avoid infinite loops */ if (f_len == 0) { if (cur[0] == '\0') break; else cur++; } if (n == limit) break; } return n; } // Backported from Glib. The only difference is that it's using gcrypt to allocate a secure buffer. static int unescape_character (const char *scanner) { int first_digit; int second_digit; first_digit = g_ascii_xdigit_value (*scanner++); if (first_digit < 0) return -1; second_digit = g_ascii_xdigit_value (*scanner++); if (second_digit < 0) return -1; return (first_digit << 4) | second_digit; } // Backported from Glib. The only difference is that it's using gcrypt to allocate a secure buffer. gchar * g_uri_unescape_string_secure (const gchar *escaped_string, const gchar *illegal_characters) { if (escaped_string == NULL) return NULL; const gchar *escaped_string_end = escaped_string + g_utf8_strlen (escaped_string, -1); gchar *result = gcry_calloc_secure (escaped_string_end - escaped_string + 1, 1); gchar *out = result; const gchar *in; gint character; for (in = escaped_string; in < escaped_string_end; in++) { character = *in; if (*in == '%') { in++; if (escaped_string_end - in < 2) { // Invalid escaped char (to short) gcry_free (result); return NULL; } character = unescape_character (in); // Check for an illegal character. We consider '\0' illegal here. if (character <= 0 || (illegal_characters != NULL && strchr (illegal_characters, (char)character) != NULL)) { gcry_free (result); return NULL; } in++; // The other char will be eaten in the loop header } *out++ = (char)character; } *out = '\0'; return result; } guchar * g_base64_decode_secure (const gchar *text, gsize *out_len) { guchar *ret; gsize input_length; gint state = 0; guint save = 0; g_return_val_if_fail (text != NULL, NULL); g_return_val_if_fail (out_len != NULL, NULL); input_length = g_utf8_strlen (text, -1); /* We can use a smaller limit here, since we know the saved state is 0, +1 used to avoid calling g_malloc0(0), and hence returning NULL */ ret = gcry_calloc_secure ((input_length / 4) * 3 + 1, 1); *out_len = g_base64_decode_step (text, input_length, ret, &state, &save); return ret; } GSList * decode_migration_data (const gchar *encoded_uri) { const gchar *encoded_uri_copy = encoded_uri; if (g_ascii_strncasecmp (encoded_uri_copy, "otpauth-migration://offline?data=", 33) != 0) { return NULL; } encoded_uri_copy += 33; gsize out_len; gchar *unesc_str = g_uri_unescape_string_secure (encoded_uri_copy, NULL); guchar *data = g_base64_decode_secure (unesc_str, &out_len); gcry_free (unesc_str); GSList *uris = NULL; GString *uri = NULL; MigrationPayload *msg = migration_payload__unpack (NULL, out_len, data); gcry_free (data); for (gint i = 0; i < msg->n_otp_parameters; i++) { uri = g_string_new ("otpauth://"); if (msg->otp_parameters[i]->type == 1) { g_string_append (uri, "hotp/"); } else if (msg->otp_parameters[i]->type == 2) { g_string_append (uri, "totp/"); } else { g_printerr ("OTP type not recognized, skipping %s\n", msg->otp_parameters[i]->name); goto end; } g_string_append (uri, msg->otp_parameters[i]->name); g_string_append (uri, "?"); if (msg->otp_parameters[i]->algorithm == 1) { g_string_append (uri, "algorithm=SHA1&"); } else if (msg->otp_parameters[i]->algorithm == 2) { g_string_append (uri, "algorithm=SHA256&"); } else if (msg->otp_parameters[i]->algorithm == 3) { g_string_append (uri, "algorithm=SHA512&"); } else { g_printerr ("Algorithm type not supported, skipping %s\n", msg->otp_parameters[i]->name); goto end; } if (msg->otp_parameters[i]->digits == 1) { g_string_append (uri, "digits=6&"); } else if (msg->otp_parameters[i]->digits == 2) { g_string_append (uri, "digits=8&"); } else { g_printerr ("Algorithm type not supported, skipping %s\n", msg->otp_parameters[i]->name); goto end; } if (msg->otp_parameters[i]->issuer != NULL) { g_string_append (uri, "issuer="); g_string_append (uri, msg->otp_parameters[i]->issuer); g_string_append (uri, "&"); } if (msg->otp_parameters[i]->type == 1) { g_string_append (uri, "counter="); g_string_append_printf(uri, "%ld", msg->otp_parameters[i]->counter); g_string_append (uri, "&"); } #ifdef COTP_OLD_LIB baseencode_error_t b_err; #else cotp_error_t b_err; #endif gchar *b32_encoded_secret = base32_encode (msg->otp_parameters[i]->secret.data, msg->otp_parameters[i]->secret.len, &b_err); if (b32_encoded_secret == NULL) { g_printerr ("Error while encoding the secret (error code %d)\n", b_err); goto end; } g_string_append (uri, "secret="); g_string_append (uri, b32_encoded_secret); uris = g_slist_append (uris, g_strdup (uri->str)); end: g_string_free (uri, TRUE); } migration_payload__free_unpacked (msg, NULL); return uris; } gcry_cipher_hd_t open_cipher_and_set_data (guchar *derived_key, guchar *iv, gsize iv_len) { gcry_cipher_hd_t hd; gpg_error_t gpg_err = gcry_cipher_open (&hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_GCM, GCRY_CIPHER_SECURE); if (gpg_err) { g_printerr ("%s\n", _("Error while opening the cipher handle.")); return NULL; } gpg_err = gcry_cipher_setkey (hd, derived_key, gcry_cipher_get_algo_keylen (GCRY_CIPHER_AES256)); if (gpg_err) { g_printerr ("%s\n", _("Error while setting the cipher key.")); gcry_cipher_close (hd); return NULL; } gpg_err = gcry_cipher_setiv (hd, iv, iv_len); if (gpg_err) { g_printerr ("%s\n", _("Error while setting the cipher iv.")); gcry_cipher_close (hd); return NULL; } return hd; } GKeyFile * get_kf_ptr (void) { GError *err = NULL; GKeyFile *kf = g_key_file_new (); gchar *cfg_file_path; #ifndef USE_FLATPAK_APP_FOLDER cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); #else cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); #endif if (g_file_test (cfg_file_path, G_FILE_TEST_EXISTS)) { if (g_key_file_load_from_file (kf, cfg_file_path, G_KEY_FILE_NONE, &err)) { g_free (cfg_file_path); return kf; } g_printerr ("%s\n", err->message); g_clear_error (&err); } g_free (cfg_file_path); g_key_file_free (kf); return NULL; }OTPClient-3.2.1/src/common/common.h000066400000000000000000000036361452112020400170350ustar00rootroot00000000000000#pragma once #include #include #include G_BEGIN_DECLS #if GLIB_CHECK_VERSION(2, 68, 0) #define g_memdupX g_memdup2 #else #define g_memdupX g_memdup #endif #define LOW_MEMLOCK_VALUE 65536 //64KB #define MEMLOCK_VALUE 67108864 //64MB gint32 get_max_file_size_from_memlock (void); gchar *init_libs (gint32 max_file_size); gint get_algo_int_from_str (const gchar *algo); guint32 jenkins_one_at_a_time_hash (const gchar *key, gsize len); guint32 json_object_get_hash (json_t *obj); gchar *secure_strdup (const gchar *src); gchar *g_trim_whitespace (const gchar *str); guchar *hexstr_to_bytes (const gchar *hexstr); gchar *bytes_to_hexstr (const guchar *data, size_t datalen); GSList *decode_migration_data (const gchar *encoded_uri); guint g_string_replace_backported (GString *string, const gchar *find, const gchar *replace, guint limit); gchar *g_uri_unescape_string_secure (const gchar *escaped_string, const gchar *illegal_characters); guchar *g_base64_decode_secure (const gchar *text, gsize *out_len); gcry_cipher_hd_t open_cipher_and_set_data (guchar *derived_key, guchar *iv, gsize iv_len); GKeyFile *get_kf_ptr (void); G_END_DECLS OTPClient-3.2.1/src/common/exports.h000066400000000000000000000020231452112020400172360ustar00rootroot00000000000000#pragma once #include #include G_BEGIN_DECLS #define ANDOTP_EXPORT_ACTION_NAME "export_andotp" #define ANDOTP_EXPORT_PLAIN_ACTION_NAME "export_andotp_plain" #define FREEOTPPLUS_EXPORT_ACTION_NAME "export_freeotpplus" #define AEGIS_EXPORT_ACTION_NAME "export_aegis" #define AEGIS_EXPORT_PLAIN_ACTION_NAME "export_aegis_plain" void export_data_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); gchar *export_andotp (const gchar *export_path, const gchar *password, json_t *json_db_data); gchar *export_freeotpplus (const gchar *export_path, json_t *json_db_data); gchar *export_aegis (const gchar *export_path, json_t *json_db_data, const gchar *password); G_END_DECLS OTPClient-3.2.1/src/common/freeotp.c000066400000000000000000000034531452112020400172010ustar00rootroot00000000000000#include #include #include #include #include "../file-size.h" #include "../parse-uri.h" #include "../gquarks.h" GSList * get_freeotpplus_data (const gchar *path, GError **err) { GSList *otps = NULL; goffset fs = get_file_size (path); if (fs < 10) { g_printerr ("Couldn't get the file size (file doesn't exit or wrong file selected\n"); return NULL; } gchar *sec_buf = gcry_calloc_secure (fs, 1); if (!g_file_get_contents (path, &sec_buf, NULL, err)) { g_printerr("Couldn't read into memory the freeotp txt file\n"); gcry_free (sec_buf); return NULL; } set_otps_from_uris (sec_buf, &otps); gcry_free (sec_buf); return otps; } gchar * export_freeotpplus (const gchar *export_path, json_t *json_db_data) { json_t *db_obj; gsize index; GError *err = NULL; GFile *out_gfile = g_file_new_for_path (export_path); GFileOutputStream *out_stream = g_file_replace (out_gfile, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION | G_FILE_CREATE_PRIVATE, NULL, &err); if (err == NULL) { json_array_foreach (json_db_data, index, db_obj) { gchar *uri = get_otpauth_uri (NULL, db_obj); if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), uri, g_utf8_strlen (uri, -1) + 1, NULL, &err) == -1) { g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "couldn't dump json data to file"); } g_free (uri); } } else { g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, "couldn't create the file object"); } g_object_unref (out_stream); g_object_unref (out_gfile); return (err != NULL ? g_strdup (err->message) : NULL); } OTPClient-3.2.1/src/common/get-providers-data.h000066400000000000000000000014061452112020400212370ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS GSList *get_andotp_data (const gchar *path, const gchar *password, gint32 max_file_size, gboolean encrypted, GError **err); GSList *get_freeotpplus_data (const gchar *path, GError **err); GSList *get_aegis_data (const gchar *path, const gchar *password, gint32 max_file_size, gboolean encrypted, GError **err); G_END_DECLS OTPClient-3.2.1/src/common/version.h.in000066400000000000000000000003071452112020400176270ustar00rootroot00000000000000#pragma once #define PROJECT_NAME "@PROJECT_NAME@" #define PROJECT_VER "@PROJECT_VERSION@" #define INSTALL_PREFIX "@CMAKE_INSTALL_PREFIX@" #define LOCALE_DIR "@SHARE_INSTALL_PREFIX@/locale"OTPClient-3.2.1/src/data.h000066400000000000000000000023411452112020400151560ustar00rootroot00000000000000#pragma once #include #include #define DBUS_SERVICES 4 G_BEGIN_DECLS typedef struct db_data_t { gchar *db_path; gchar *key; json_t *json_data; GSList *objects_hash; GSList *data_to_add; gint32 max_file_size_from_memlock; gchar *last_hotp; GDateTime *last_hotp_update; gboolean key_stored; } DatabaseData; typedef struct app_data_t { GtkBuilder *builder; GtkWidget *main_window; GtkWidget *info_bar; GtkTreeView *tree_view; GtkClipboard *clipboard; gboolean show_next_otp; gboolean disable_notifications; gint search_column; gboolean auto_lock; gint inactivity_timeout; GtkCssProvider *css_provider; GNotification *notification; guint source_id; guint source_id_last_activity; DatabaseData *db_data; GDBusConnection *connection; guint subscription_ids[DBUS_SERVICES]; gboolean app_locked; gboolean use_dark_theme; gboolean is_reorder_active; gboolean use_secret_service; GDateTime *last_user_activity; GtkWidget *diag_rcdb; GtkFileChooserAction open_db_file_action; } AppData; typedef struct node_info_t { guint hash; guint newpos; } NodeInfo; G_END_DECLS OTPClient-3.2.1/src/db-actions.c000066400000000000000000000052101452112020400162610ustar00rootroot00000000000000#include #include #include "data.h" #include "message-dialogs.h" #include "db-actions.h" void select_file_icon_pressed_cb (GtkEntry *entry, gint position __attribute__((unused)), GdkEventButton *event __attribute__((unused)), gpointer data) { AppData *app_data = (AppData *)data; #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wbad-function-cast" gint action_int = GPOINTER_TO_INT(g_object_get_data (G_OBJECT(entry), "action")); #pragma GCC diagnostic pop GtkFileChooserAction action = (action_int == ACTION_OPEN) ? GTK_FILE_CHOOSER_ACTION_OPEN : GTK_FILE_CHOOSER_ACTION_SAVE; GtkFileChooserNative *dialog = gtk_file_chooser_native_new ("Select database", GTK_WINDOW(app_data->main_window), action, "OK", "Cancel"); GFile *gfile_dbpath = g_file_new_for_path (app_data->db_data->db_path); gchar *db_dir = g_file_get_path (g_file_get_parent (gfile_dbpath)); gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER(dialog), db_dir); gint res = gtk_native_dialog_run (GTK_NATIVE_DIALOG(dialog)); if (res == GTK_RESPONSE_ACCEPT) { gtk_entry_set_text (entry, gtk_file_chooser_get_filename (GTK_FILE_CHOOSER(dialog))); } g_free (db_dir); g_object_unref (gfile_dbpath); g_object_unref (dialog); } void update_cfg_file (AppData *app_data) { GKeyFile *kf = g_key_file_new (); gchar *cfg_file_path; #ifndef USE_FLATPAK_APP_FOLDER cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); #else cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); #endif if (!g_key_file_load_from_file (kf, cfg_file_path, G_KEY_FILE_NONE, NULL)) { g_printerr ("%s\n", _("Error while loading the config file.")); } g_key_file_set_string (kf, "config", "db_path", app_data->db_data->db_path); GError *cfg_err = NULL; if (!g_key_file_save_to_file (kf, cfg_file_path, &cfg_err)) { if (cfg_err != NULL) { gchar *msg = g_strconcat ("Couldn't save the change to the config file: ", &cfg_err->message, NULL); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); g_clear_error (&cfg_err); } } g_free (cfg_file_path); g_key_file_free (kf); } OTPClient-3.2.1/src/db-actions.h000066400000000000000000000006451452112020400162750ustar00rootroot00000000000000#pragma once #include #include "data.h" G_BEGIN_DECLS #define ACTION_OPEN 5 #define ACTION_SAVE 10 void select_file_icon_pressed_cb (GtkEntry *entry, gint position, GdkEventButton *event, gpointer data); void update_cfg_file (AppData *app_data); G_END_DECLSOTPClient-3.2.1/src/db-misc.c000066400000000000000000000371751452112020400155730ustar00rootroot00000000000000#include #include #include #include #include #include "db-misc.h" #include "otpclient.h" #include "file-size.h" #include "gquarks.h" #include "common/common.h" typedef struct header_data_t { guint8 iv[IV_SIZE]; guint8 salt[KDF_SALT_SIZE]; } HeaderData; static void reload_db (DatabaseData *db_data, GError **err); static void update_db (DatabaseData *db_data, GError **err); static gpointer encrypt_db (const gchar *db_path, const gchar *in_memory_json, const gchar *password, GError **err); static inline void add_to_json (gpointer list_elem, gpointer json_array); static gchar *decrypt_db (const gchar *db_path, const gchar *password); static guchar *get_derived_key (const gchar *pwd, HeaderData *header_data); static void backup_db (const gchar *path); static void restore_db (const gchar *path); static inline void json_free (gpointer data); static void cleanup (GFile *, gpointer, HeaderData *, GError *); void load_db (DatabaseData *db_data, GError **err) { if (!g_file_test (db_data->db_path, G_FILE_TEST_EXISTS)) { g_set_error (err, missing_file_gquark (), MISSING_FILE_CODE, "Missing database file"); db_data->json_data = NULL; return; } gchar *in_memory_json = decrypt_db (db_data->db_path, db_data->key); if (in_memory_json == TAG_MISMATCH) { g_set_error (err, bad_tag_gquark (), BAD_TAG_ERRCODE, "Either the file is corrupted or the password is wrong"); return; } else if (in_memory_json == KEY_DERIV_ERR) { g_set_error (err, key_deriv_gquark (), KEY_DERIVATION_ERRCODE, "Error during key derivation"); return; } else if (in_memory_json == GENERIC_ERROR) { g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "An error occurred, please check stderr"); return; } json_error_t jerr; db_data->json_data = json_loads (in_memory_json, 0, &jerr); gcry_free (in_memory_json); if (db_data->json_data == NULL) { gchar *msg = g_strconcat ("Error while loading json data: ", jerr.text, NULL); g_set_error (err, memlock_error_gquark(), MEMLOCK_ERRCODE, "%s", msg); return; } gsize index; json_t *obj; json_array_foreach (db_data->json_data, index, obj) { guint32 hash = json_object_get_hash (obj); db_data->objects_hash = g_slist_append (db_data->objects_hash, g_memdupX (&hash, sizeof (guint32))); } } void update_and_reload_db (AppData *app_data, DatabaseData *db_data, gboolean regenerate_model, GError **err) { update_db (db_data, err); if (*err != NULL && !g_error_matches (*err, missing_file_gquark (), MISSING_FILE_CODE)) { g_printerr ("%s\n", (*err)->message); return; } reload_db (db_data, err); if (*err != NULL && !g_error_matches (*err, missing_file_gquark (), MISSING_FILE_CODE)) { g_printerr ("%s\n", (*err)->message); return; } #ifdef BUILD_GUI if (regenerate_model) { update_model (app_data); g_slist_free_full (app_data->db_data->data_to_add, json_free); app_data->db_data->data_to_add = NULL; } #endif } void load_new_db (AppData *app_data, GError **err) { reload_db (app_data->db_data, err); if (*err != NULL) { return; } #ifdef BUILD_GUI update_model (app_data); g_slist_free_full (app_data->db_data->data_to_add, json_free); app_data->db_data->data_to_add = NULL; #endif } gint check_duplicate (gconstpointer data, gconstpointer user_data) { guint list_elem = *(guint *)data; if (list_elem == GPOINTER_TO_UINT(user_data)) { return 0; } return -1; } void write_db_to_disk (DatabaseData *db_data, GError **err) { update_db (db_data, err); } static void reload_db (DatabaseData *db_data, GError **err) { if (db_data->json_data != NULL) { json_decref (db_data->json_data); } g_slist_free_full (db_data->objects_hash, g_free); db_data->objects_hash = NULL; load_db (db_data, err); } static void update_db (DatabaseData *db_data, GError **err) { gboolean first_run = (db_data->json_data == NULL) ? TRUE : FALSE; if (first_run == TRUE) { db_data->json_data = json_array (); } else { // database is backed-up only if this is not the first run backup_db (db_data->db_path); } g_slist_foreach (db_data->data_to_add, add_to_json, db_data->json_data); gchar *plain_data = json_dumps (db_data->json_data, JSON_COMPACT); if (encrypt_db (db_data->db_path, plain_data, db_data->key, err) != NULL) { if (!first_run) { g_printerr ("Encrypting the new data failed, restoring original copy...\n"); restore_db (db_data->db_path); } else { g_printerr ("Couldn't update the database (encrypt_db failed)\n"); if (g_file_test (db_data->db_path, G_FILE_TEST_EXISTS)) { if (g_unlink (db_data->db_path) == -1) { g_printerr ("%s\n", _("Error while unlinking the file.")); } } } } else { // database must be backed-up both before and after the update backup_db (db_data->db_path); } gcry_free (plain_data); } static inline void add_to_json (gpointer list_elem, gpointer json_array) { json_array_append (json_array, json_deep_copy (list_elem)); } static gpointer encrypt_db (const gchar *db_path, const gchar *in_memory_json, const gchar *password, GError **err) { GError *local_err = NULL; HeaderData *header_data = g_new0 (HeaderData, 1); gcry_create_nonce (header_data->iv, IV_SIZE); gcry_create_nonce (header_data->salt, KDF_SALT_SIZE); GFile *out_file = g_file_new_for_path (db_path); GFileOutputStream *out_stream = g_file_replace (out_file, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, &local_err); if (local_err != NULL) { g_printerr ("%s\n", local_err->message); g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "Failed to replace existing file"); cleanup (out_file, NULL, header_data, local_err); return GENERIC_ERROR; } if (g_output_stream_write (G_OUTPUT_STREAM (out_stream), header_data, sizeof (HeaderData), NULL, &local_err) == -1) { g_printerr ("%s\n", local_err->message); g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "Failed while writing header data to file"); cleanup (out_file, out_stream, header_data, local_err); return GENERIC_ERROR; } guchar *derived_key = get_derived_key (password, header_data); if (derived_key == SECURE_MEMORY_ALLOC_ERR || derived_key == KEY_DERIV_ERR) { cleanup (out_file, out_stream, header_data, local_err); g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "Failed to derive key.\nPlease check Secure Memory wiki page"); return (gpointer)derived_key; } gsize input_data_len = strlen (in_memory_json) + 1; guchar *enc_buffer = g_malloc0 (input_data_len); gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, header_data->iv, IV_SIZE); if (hd == NULL) { gcry_free (derived_key); g_free (header_data); g_free (enc_buffer); return NULL; } gpg_error_t gpg_err = gcry_cipher_authenticate (hd, header_data, sizeof (HeaderData)); if (gpg_err) { g_printerr ("%s\n", _("Error while processing the authenticated data.")); gcry_free (derived_key); g_free (header_data); g_free (enc_buffer); gcry_cipher_close (hd); return GENERIC_ERROR; } gpg_err = gcry_cipher_encrypt (hd, enc_buffer, input_data_len, in_memory_json, input_data_len); if (gpg_err) { g_printerr ("%s\n", _("Error while encrypting the data.")); gcry_free (derived_key); g_free (enc_buffer); g_free (header_data); gcry_cipher_close (hd); return GENERIC_ERROR; } guchar tag[TAG_SIZE]; gpg_err = gcry_cipher_gettag (hd, tag, TAG_SIZE); //append tag to outfile if (gpg_err) { g_printerr ("%s\n", _("Error while getting the tag.")); gcry_free (derived_key); g_free (enc_buffer); g_free (header_data); gcry_cipher_close (hd); return GENERIC_ERROR; } if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), enc_buffer, input_data_len, NULL, &local_err) == -1) { g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "Failed while writing encrypted buffer to file"); cleanup (out_file, out_stream, header_data, local_err); g_free (enc_buffer); gcry_free (derived_key); gcry_cipher_close (hd); return GENERIC_ERROR; } if (g_output_stream_write (G_OUTPUT_STREAM(out_stream), tag, TAG_SIZE, NULL, &local_err) == -1) { g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "Failed while writing tag data to file"); cleanup (out_file, out_stream, header_data, local_err); g_free (enc_buffer); gcry_free (derived_key); gcry_cipher_close (hd); return GENERIC_ERROR; } g_free (enc_buffer); gcry_free (derived_key); gcry_cipher_close (hd); cleanup (out_file, out_stream, header_data, NULL); return NULL; } static gchar * decrypt_db (const gchar *db_path, const gchar *password) { GError *err = NULL; HeaderData *header_data = g_new0 (HeaderData, 1); goffset input_file_size = get_file_size (db_path); GFile *in_file = g_file_new_for_path (db_path); GFileInputStream *in_stream = g_file_read (in_file, NULL, &err); if (err != NULL) { g_printerr ("%s\n", err->message); cleanup (in_file, NULL, header_data, err); return GENERIC_ERROR; } if (g_input_stream_read (G_INPUT_STREAM (in_stream), header_data, sizeof (HeaderData), NULL, &err) == -1) { g_printerr ("%s\n", err->message); cleanup (in_file, in_stream, header_data, err); return GENERIC_ERROR; } guchar tag[TAG_SIZE]; if (!g_seekable_seek (G_SEEKABLE (in_stream), input_file_size - TAG_SIZE, G_SEEK_SET, NULL, &err)) { g_printerr ("%s\n", err->message); cleanup (in_file, in_stream, header_data, err); return GENERIC_ERROR; } if (g_input_stream_read (G_INPUT_STREAM (in_stream), tag, TAG_SIZE, NULL, &err) == -1) { g_printerr ("%s\n", err->message); cleanup (in_file, in_stream, header_data, err); return GENERIC_ERROR; } gsize enc_buf_size = input_file_size - sizeof (HeaderData) - TAG_SIZE; guchar *enc_buf = g_malloc0 (enc_buf_size); if (!g_seekable_seek (G_SEEKABLE (in_stream), sizeof (HeaderData), G_SEEK_SET, NULL, &err)) { g_printerr ("%s\n", err->message); cleanup (in_file, in_stream, header_data, err); g_free (enc_buf); return GENERIC_ERROR; } if (g_input_stream_read (G_INPUT_STREAM (in_stream), enc_buf, enc_buf_size, NULL, &err) == -1) { g_printerr ("%s\n", err->message); cleanup (in_file, in_stream, header_data, err); g_free (enc_buf); return GENERIC_ERROR; } g_object_unref (in_stream); g_object_unref (in_file); guchar *derived_key = get_derived_key (password, header_data); if (derived_key == SECURE_MEMORY_ALLOC_ERR || derived_key == KEY_DERIV_ERR) { g_free (header_data); g_free (enc_buf); return (gpointer)derived_key; } gcry_cipher_hd_t hd = open_cipher_and_set_data (derived_key, header_data->iv, IV_SIZE); if (hd == NULL) { gcry_free (derived_key); g_free (enc_buf); g_free (header_data); return GENERIC_ERROR; } gpg_error_t gpg_err = gcry_cipher_authenticate (hd, header_data, sizeof (HeaderData)); if (gpg_err) { g_printerr ("%s\n", _("Error while processing the authenticated data.")); gcry_free (derived_key); g_free (header_data); g_free (enc_buf); gcry_cipher_close (hd); return GENERIC_ERROR; } gchar *dec_buf = gcry_calloc_secure (enc_buf_size, 1); if (dec_buf == NULL) { g_printerr ("%s\n", _("Error while allocating secure memory.")); gcry_free (derived_key); g_free (header_data); g_free (enc_buf); gcry_cipher_close (hd); return GENERIC_ERROR; } gpg_err = gcry_cipher_decrypt (hd, dec_buf, enc_buf_size, enc_buf, enc_buf_size); if (gpg_err) { g_printerr ("%s\n", _("Error while decrypting the data.")); gcry_free (derived_key); gcry_free (dec_buf); g_free (header_data); g_free (enc_buf); gcry_cipher_close (hd); return GENERIC_ERROR; } if (gcry_err_code (gcry_cipher_checktag (hd, tag, TAG_SIZE)) == GPG_ERR_CHECKSUM) { gcry_cipher_close (hd); gcry_free (derived_key); gcry_free (dec_buf); g_free (header_data); g_free (enc_buf); return TAG_MISMATCH; } gcry_cipher_close (hd); gcry_free (derived_key); g_free (header_data); g_free (enc_buf); return dec_buf; } static guchar * get_derived_key (const gchar *pwd, HeaderData *header_data) { gsize key_len = gcry_cipher_get_algo_keylen (GCRY_CIPHER_AES256); gsize pwd_len = g_utf8_strlen (pwd, -1) + 1; guchar *derived_key = gcry_malloc_secure (key_len); if (derived_key == NULL) { g_printerr ("%s\n", _("Couldn't allocate the needed secure memory.")); return SECURE_MEMORY_ALLOC_ERR; } gpg_error_t ret = gcry_kdf_derive (pwd, pwd_len, GCRY_KDF_PBKDF2, GCRY_MD_SHA512, header_data->salt, KDF_SALT_SIZE, KDF_ITERATIONS, key_len, derived_key); if (ret != 0) { gcry_free (derived_key); g_printerr ("%s\n", _("Error during key derivation.")); return KEY_DERIV_ERR; } return derived_key; } static void backup_db (const gchar *path) { GError *err = NULL; GFile *src = g_file_new_for_path (path); gchar *dst_path = g_strconcat (path, ".bak", NULL); GFile *dst = g_file_new_for_path (dst_path); g_free (dst_path); if (!g_file_copy (src, dst, G_FILE_COPY_OVERWRITE | G_FILE_COPY_NOFOLLOW_SYMLINKS, NULL, NULL, NULL, &err)) { g_printerr ("Couldn't create the backup file: %s\n", err->message); g_clear_error (&err); } g_object_unref (src); g_object_unref (dst); } static void restore_db (const gchar *path) { GError *err = NULL; gchar *src_path = g_strconcat (path, ".bak", NULL); GFile *src = g_file_new_for_path (src_path); GFile *dst = g_file_new_for_path (path); g_free (src_path); if (!g_file_copy (src, dst, G_FILE_COPY_OVERWRITE | G_FILE_COPY_NOFOLLOW_SYMLINKS, NULL, NULL, NULL, &err)) { g_printerr ("Couldn't restore the backup file: %s\n", err->message); g_clear_error (&err); } else { g_print ("%s\n", _("Backup copy successfully restored.")); } g_object_unref (src); g_object_unref (dst); } static inline void json_free (gpointer data) { json_decref (data); } static void cleanup (GFile *in_file, gpointer in_stream, HeaderData *header_data, GError *err) { g_object_unref (in_file); if (in_stream != NULL) g_object_unref (in_stream); if (header_data != NULL) g_free (header_data); if (err != NULL) g_clear_error (&err); } OTPClient-3.2.1/src/db-misc.h000066400000000000000000000020351452112020400155630ustar00rootroot00000000000000#pragma once #include #include "data.h" G_BEGIN_DECLS #define GENERIC_ERROR (gpointer)1 #define TAG_MISMATCH (gpointer)2 #define SECURE_MEMORY_ALLOC_ERR (gpointer)3 #define KEY_DERIV_ERR (gpointer)4 #define IV_SIZE 16 #define KDF_ITERATIONS 100000 #define KDF_SALT_SIZE 32 #define TAG_SIZE 16 void load_db (DatabaseData *db_data, GError **error); void update_and_reload_db (AppData *app_data, DatabaseData *db_data, gboolean regenerate_model, GError **err); void load_new_db (AppData *app_data, GError **err); void write_db_to_disk (DatabaseData *db_data, GError **err); gint check_duplicate (gconstpointer data, gconstpointer user_data); G_END_DECLS OTPClient-3.2.1/src/dbinfo-cb.c000066400000000000000000000026721452112020400160720ustar00rootroot00000000000000#include "data.h" void dbinfo_cb (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; GtkWidget *dbinfo_diag = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "dbinfo_diag_id")); GtkWidget *db_location_entry = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "dbentry_dbinfo_id")); GtkWidget *config_entry = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "configentry_dbinfo_id")); GtkWidget *num_entries_label = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "numofentries_dbinfo_id")); gtk_entry_set_text (GTK_ENTRY(db_location_entry), app_data->db_data->db_path); gchar *cfg_file_path = NULL; #ifdef USE_FLATPAK_APP_FOLDER cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); #else cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); #endif gtk_entry_set_text (GTK_ENTRY(config_entry), cfg_file_path); gchar *num_of_entries = g_strdup_printf ("%lu", json_array_size (app_data->db_data->json_data)); gtk_label_set_text (GTK_LABEL(num_entries_label), num_of_entries); gint result = gtk_dialog_run (GTK_DIALOG(dbinfo_diag)); if (result == GTK_RESPONSE_CLOSE) { g_free (cfg_file_path); g_free (num_of_entries); gtk_widget_hide (dbinfo_diag); } }OTPClient-3.2.1/src/dbinfo-cb.h000066400000000000000000000003071452112020400160700ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS void dbinfo_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); G_END_DECLSOTPClient-3.2.1/src/edit-row-cb.c000066400000000000000000000214621452112020400163610ustar00rootroot00000000000000#include #include #include "imports.h" #include "treeview.h" #include "db-misc.h" #include "get-builder.h" #include "message-dialogs.h" #include "gui-common.h" #include "gquarks.h" #include "common/common.h" typedef struct edit_data_t { GtkListStore *list_store; GtkTreeIter iter; DatabaseData *db_data; gchar *current_label; gchar *new_label; gchar *current_issuer; gchar *new_issuer; gchar *current_secret; gchar *new_secret; } EditData; static void show_edit_dialog (EditData *edit_data, AppData *app_data); static void set_entry_editability (GtkToolButton *btn, gpointer user_data); static gchar *get_parse_and_set_data_from_entries (EditData *edit_data, GtkWidget *lab_ck_btn, GtkWidget *new_lab_entry, GtkWidget *iss_ck_btn, GtkWidget *new_iss_entry, GtkWidget *sec_ck_btn, GtkWidget *new_sec_entry); static void set_data_in_lstore_and_json (EditData *edit_data); void edit_row_cb (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { EditData *edit_data = g_new0 (EditData, 1); AppData *app_data = (AppData *)user_data; edit_data->db_data = app_data->db_data; GtkTreeModel *model = gtk_tree_view_get_model (app_data->tree_view); edit_data->list_store = GTK_LIST_STORE(model); if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (app_data->tree_view), &model, &edit_data->iter)) { gtk_tree_model_get (model, &edit_data->iter, COLUMN_ACC_LABEL, &edit_data->current_label, COLUMN_ACC_ISSUER, &edit_data->current_issuer, -1); show_edit_dialog (edit_data, app_data); g_free (edit_data->current_label); g_free (edit_data->current_issuer); gcry_free (edit_data->current_secret); } GError *err = NULL; update_and_reload_db (app_data, app_data->db_data, TRUE, &err); if (err != NULL && !g_error_matches (err, missing_file_gquark (), MISSING_FILE_CODE)) { show_message_dialog (app_data->main_window, err->message, GTK_MESSAGE_ERROR); } g_free (edit_data->new_label); g_free (edit_data->new_issuer); if (edit_data->new_secret != NULL) { gcry_free (edit_data->new_secret); } g_free (edit_data); } void edit_row_cb_shortcut (GtkWidget *w __attribute__((unused)), gpointer user_data) { edit_row_cb (NULL, NULL, user_data); } static void show_edit_dialog (EditData *edit_data, AppData *app_data) { GtkBuilder *builder = get_builder_from_partial_path (UI_PARTIAL_PATH); GtkWidget *diag = GTK_WIDGET (gtk_builder_get_object (builder, "edit_diag_id")); gtk_window_set_transient_for (GTK_WINDOW(diag), GTK_WINDOW(app_data->main_window)); GtkWidget *new_lab_entry = GTK_WIDGET (gtk_builder_get_object (builder, "entry_newlabel_id")); GtkWidget *new_iss_entry = GTK_WIDGET (gtk_builder_get_object (builder, "entry_newissuer_id")); GtkWidget *new_sec_entry = GTK_WIDGET (gtk_builder_get_object (builder, "entry_newsec_id")); g_signal_connect (new_sec_entry, "icon-press", G_CALLBACK (icon_press_cb), NULL); if (edit_data->current_label != NULL) { gtk_entry_set_text (GTK_ENTRY(new_lab_entry), edit_data->current_label); } if (edit_data->current_issuer != NULL) { gtk_entry_set_text (GTK_ENTRY(new_iss_entry), edit_data->current_issuer); if (g_ascii_strcasecmp (edit_data->current_issuer, "steam") == 0) { gtk_widget_set_sensitive (new_iss_entry, FALSE); } } guint row_number = get_row_number_from_iter (edit_data->list_store, edit_data->iter); json_t *obj = json_array_get (edit_data->db_data->json_data, row_number); edit_data->current_secret = secure_strdup (json_string_value (json_object_get (obj, "secret"))); if (edit_data->current_secret != NULL) { gtk_entry_set_text (GTK_ENTRY(new_sec_entry), edit_data->current_secret); } GtkWidget *lab_ck_btn = GTK_WIDGET (gtk_builder_get_object (builder, "label_check_id")); g_signal_connect (lab_ck_btn, "toggled", G_CALLBACK (set_entry_editability), new_lab_entry); GtkWidget *iss_ck_btn = GTK_WIDGET (gtk_builder_get_object (builder, "issuer_check_id")); g_signal_connect (iss_ck_btn, "toggled", G_CALLBACK (set_entry_editability), new_iss_entry); GtkWidget *sec_ck_btn = GTK_WIDGET (gtk_builder_get_object (builder, "secret_check_id")); g_signal_connect (sec_ck_btn, "toggled", G_CALLBACK (set_entry_editability), new_sec_entry); gchar *err_msg = NULL; gint res = gtk_dialog_run (GTK_DIALOG (diag)); switch (res) { case GTK_RESPONSE_OK: err_msg = get_parse_and_set_data_from_entries (edit_data, lab_ck_btn, new_lab_entry, iss_ck_btn, new_iss_entry, sec_ck_btn, new_sec_entry); if (err_msg != NULL) { show_message_dialog (app_data->main_window, err_msg, GTK_MESSAGE_ERROR); g_free (err_msg); } break; case GTK_RESPONSE_CANCEL: default: break; } gtk_widget_destroy (diag); g_object_unref (builder); } static void set_entry_editability (GtkToolButton *btn __attribute__((unused)), gpointer user_data) { gtk_editable_set_editable (GTK_EDITABLE (user_data), !gtk_editable_get_editable(user_data)); } static gchar * get_parse_and_set_data_from_entries (EditData *edit_data, GtkWidget *lab_ck_btn, GtkWidget *new_lab_entry, GtkWidget *iss_ck_btn, GtkWidget *new_iss_entry, GtkWidget *sec_ck_btn, GtkWidget *new_sec_entry) { edit_data->new_label = g_strdup (gtk_entry_get_text (GTK_ENTRY (new_lab_entry))); edit_data->new_issuer = g_strdup (gtk_entry_get_text (GTK_ENTRY (new_iss_entry))); edit_data->new_secret = secure_strdup (gtk_entry_get_text (GTK_ENTRY (new_sec_entry))); if (g_utf8_strlen (edit_data->new_issuer, -1) > 0) { if (!g_str_is_ascii (edit_data->new_issuer)) { return g_strdup ("Issuer entry must contain only ASCII characters."); } } if (g_utf8_strlen (edit_data->new_label, -1) < 1 || !g_str_is_ascii (edit_data->new_label)) { return g_strdup ("Label must not be empty and must contain only ASCII characters."); } if (!g_str_is_ascii (edit_data->new_secret) || g_utf8_strlen (edit_data->new_secret, -1) < 6) { return g_strdup ("The secret must be at least 6 characters long and must contain only ASCII characters."); } if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(lab_ck_btn)) || g_strcmp0 (edit_data->current_label, edit_data->new_label) == 0) { g_free (edit_data->new_label); edit_data->new_label = NULL; } if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(iss_ck_btn)) || g_strcmp0 (edit_data->current_issuer, edit_data->new_issuer) == 0) { g_free (edit_data->new_issuer); edit_data->new_issuer = NULL; } if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(sec_ck_btn)) || g_strcmp0 (edit_data->current_secret, edit_data->new_secret) == 0) { gcry_free (edit_data->new_secret); edit_data->new_secret = NULL; } set_data_in_lstore_and_json (edit_data); return NULL; } static void set_data_in_lstore_and_json (EditData *edit_data) { guint row_number = get_row_number_from_iter (edit_data->list_store, edit_data->iter); json_t *obj = json_array_get (edit_data->db_data->json_data, row_number); if (edit_data->new_label != NULL) { gtk_list_store_set (edit_data->list_store, &edit_data->iter, COLUMN_ACC_LABEL, edit_data->new_label, -1); json_object_set (obj, "label", json_string (edit_data->new_label)); } if (edit_data->new_issuer != NULL) { gtk_list_store_set (edit_data->list_store, &edit_data->iter, COLUMN_ACC_ISSUER, edit_data->new_issuer, -1); json_object_set (obj, "issuer", json_string (edit_data->new_issuer)); } if (edit_data->new_secret != NULL) { json_object_set (obj, "secret", json_string (edit_data->new_secret)); } }OTPClient-3.2.1/src/edit-row-cb.h000066400000000000000000000005311452112020400163600ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS void edit_row_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void edit_row_cb_shortcut (GtkWidget *w, gpointer user_data); G_END_DECLSOTPClient-3.2.1/src/exports.c000066400000000000000000000116301452112020400157450ustar00rootroot00000000000000#include #include #include #include "password-cb.h" #include "message-dialogs.h" #include "common/exports.h" static void show_ret_msg_dialog (GtkWidget *mainwin, const gchar *fpath, const gchar *ret_msg); void export_data_cb (GSimpleAction *simple, GVariant *parameter __attribute__((unused)), gpointer user_data) { const gchar *action_name = g_action_get_name (G_ACTION(simple)); AppData *app_data = (AppData *)user_data; const gchar *base_dir = NULL; #ifndef USE_FLATPAK_APP_FOLDER base_dir = g_get_home_dir (); #else base_dir = g_get_user_data_dir (); #endif gboolean encrypted; if ((g_strcmp0 (action_name, "export_andotp") == 0) || (g_strcmp0 (action_name, "export_aegis") == 0)) { encrypted = TRUE; } else { encrypted = FALSE; } GtkFileChooserNative *fl_diag = gtk_file_chooser_native_new ("Export file", GTK_WINDOW(app_data->main_window), GTK_FILE_CHOOSER_ACTION_SAVE, "OK", "Cancel"); gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER(fl_diag), base_dir); gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER(fl_diag), TRUE); gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER(fl_diag), FALSE); const gchar *filename = NULL; if (g_strcmp0 (action_name, ANDOTP_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, ANDOTP_EXPORT_PLAIN_ACTION_NAME) == 0) { filename = (encrypted == TRUE) ? "andotp_exports.json.aes" : "andotp_exports.json"; } else if (g_strcmp0 (action_name, FREEOTPPLUS_EXPORT_ACTION_NAME) == 0) { filename = "freeotpplus-exports.txt"; } else if (g_strcmp0 (action_name, AEGIS_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_EXPORT_PLAIN_ACTION_NAME) == 0) { filename = (encrypted == TRUE) ? "aegis_encrypted.json" : "aegis_export_plain.json"; } else { show_message_dialog (app_data->main_window, "Invalid export action.", GTK_MESSAGE_ERROR); return; } gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER(fl_diag), filename); gchar *export_file_abs_path = NULL; gint native_diag_res = gtk_native_dialog_run (GTK_NATIVE_DIALOG(fl_diag)); if (native_diag_res == GTK_RESPONSE_ACCEPT) { export_file_abs_path = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER(fl_diag)); } g_object_unref (fl_diag); if (export_file_abs_path == NULL) { show_message_dialog (app_data->main_window, "Invalid export file name/path.", GTK_MESSAGE_ERROR); return; } gchar *password = NULL, *ret_msg = NULL; if (g_strcmp0 (action_name, ANDOTP_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, ANDOTP_EXPORT_PLAIN_ACTION_NAME) == 0) { if (encrypted == TRUE) { password = prompt_for_password (app_data, NULL, NULL, TRUE); if (password == NULL) { return; } } ret_msg = export_andotp (export_file_abs_path, password, app_data->db_data->json_data); show_ret_msg_dialog (app_data->main_window, export_file_abs_path, ret_msg); } else if (g_strcmp0 (action_name, FREEOTPPLUS_EXPORT_ACTION_NAME) == 0) { ret_msg = export_freeotpplus (export_file_abs_path, app_data->db_data->json_data); show_ret_msg_dialog (app_data->main_window, export_file_abs_path, ret_msg); } else if (g_strcmp0 (action_name, AEGIS_EXPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_EXPORT_PLAIN_ACTION_NAME) == 0) { if (encrypted == TRUE) { password = prompt_for_password (app_data, NULL, NULL, TRUE); if (password == NULL) { return; } } ret_msg = export_aegis (export_file_abs_path, app_data->db_data->json_data, password); show_ret_msg_dialog (app_data->main_window, export_file_abs_path, ret_msg); } else { show_message_dialog (app_data->main_window, "Invalid export action.", GTK_MESSAGE_ERROR); return; } g_free (ret_msg); g_free (export_file_abs_path); if (encrypted == TRUE) { gcry_free (password); } } static void show_ret_msg_dialog (GtkWidget *mainwin, const gchar *fpath, const gchar *ret_msg) { GtkMessageType msg_type; gchar *message = NULL; if (ret_msg != NULL) { message = g_strconcat ("Error while exporting data: ", ret_msg, NULL); msg_type = GTK_MESSAGE_ERROR; } else { message = g_strconcat ("Data successfully exported to ", fpath, NULL); msg_type = GTK_MESSAGE_INFO; } show_message_dialog (mainwin, message, msg_type); g_free (message); } OTPClient-3.2.1/src/file-size.c000066400000000000000000000007531452112020400161340ustar00rootroot00000000000000#include goffset get_file_size (const gchar *file_path) { GError *error = NULL; GFile *file = g_file_new_for_path (file_path); GFileInfo *info = g_file_query_info (G_FILE(file), "standard::*", G_FILE_QUERY_INFO_NONE, NULL, &error); if (info == NULL) { g_printerr ("%s\n", error->message); g_clear_error (&error); return -1; } goffset file_size = g_file_info_get_size (info); g_object_unref (file); return file_size; } OTPClient-3.2.1/src/file-size.h000066400000000000000000000001251452112020400161320ustar00rootroot00000000000000#pragma once G_BEGIN_DECLS goffset get_file_size (const gchar *path); G_END_DECLS OTPClient-3.2.1/src/get-builder.c000066400000000000000000000007341452112020400164470ustar00rootroot00000000000000#include #include "version.h" GtkBuilder * get_builder_from_partial_path (const gchar *partial_path) { const gchar *prefix; #ifndef USE_FLATPAK_APP_FOLDER // cmake trims the last '/', so we have to manually add it later on prefix = INSTALL_PREFIX; #else prefix = "/app"; #endif gchar *path = g_strconcat (prefix, "/", partial_path, NULL); GtkBuilder *builder = gtk_builder_new_from_file (path); g_free (path); return builder; } OTPClient-3.2.1/src/get-builder.h000066400000000000000000000002611452112020400164470ustar00rootroot00000000000000#pragma once G_BEGIN_DECLS #define UI_PARTIAL_PATH "share/otpclient/otpclient.ui" GtkBuilder *get_builder_from_partial_path (const gchar *partial_path); G_END_DECLS OTPClient-3.2.1/src/google-migration.pb-c.c000066400000000000000000000252631452112020400203330ustar00rootroot00000000000000/* Generated by the protocol buffer compiler. DO NOT EDIT! */ /* Generated from: data/google-migration.proto */ /* Do not generate deprecated warnings for self */ #ifndef PROTOBUF_C__NO_DEPRECATED #define PROTOBUF_C__NO_DEPRECATED #endif #include "google-migration.pb-c.h" void migration_payload__otp_parameters__init (MigrationPayload__OtpParameters *message) { static const MigrationPayload__OtpParameters init_value = MIGRATION_PAYLOAD__OTP_PARAMETERS__INIT; *message = init_value; } void migration_payload__init (MigrationPayload *message) { static const MigrationPayload init_value = MIGRATION_PAYLOAD__INIT; *message = init_value; } size_t migration_payload__get_packed_size (const MigrationPayload *message) { assert(message->base.descriptor == &migration_payload__descriptor); return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); } size_t migration_payload__pack (const MigrationPayload *message, uint8_t *out) { assert(message->base.descriptor == &migration_payload__descriptor); return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); } size_t migration_payload__pack_to_buffer (const MigrationPayload *message, ProtobufCBuffer *buffer) { assert(message->base.descriptor == &migration_payload__descriptor); return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); } MigrationPayload * migration_payload__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data) { return (MigrationPayload *) protobuf_c_message_unpack (&migration_payload__descriptor, allocator, len, data); } void migration_payload__free_unpacked (MigrationPayload *message, ProtobufCAllocator *allocator) { if(!message) return; assert(message->base.descriptor == &migration_payload__descriptor); protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); } static const ProtobufCFieldDescriptor migration_payload__otp_parameters__field_descriptors[7] = { { "secret", 1, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_BYTES, 0, /* quantifier_offset */ offsetof(MigrationPayload__OtpParameters, secret), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "name", 2, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_STRING, 0, /* quantifier_offset */ offsetof(MigrationPayload__OtpParameters, name), NULL, &protobuf_c_empty_string, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "issuer", 3, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_STRING, 0, /* quantifier_offset */ offsetof(MigrationPayload__OtpParameters, issuer), NULL, &protobuf_c_empty_string, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "algorithm", 4, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_ENUM, 0, /* quantifier_offset */ offsetof(MigrationPayload__OtpParameters, algorithm), &migration_payload__algorithm__descriptor, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "digits", 5, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_ENUM, 0, /* quantifier_offset */ offsetof(MigrationPayload__OtpParameters, digits), &migration_payload__digit_count__descriptor, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "type", 6, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_ENUM, 0, /* quantifier_offset */ offsetof(MigrationPayload__OtpParameters, type), &migration_payload__otp_type__descriptor, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "counter", 7, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_INT64, 0, /* quantifier_offset */ offsetof(MigrationPayload__OtpParameters, counter), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned migration_payload__otp_parameters__field_indices_by_name[] = { 3, /* field[3] = algorithm */ 6, /* field[6] = counter */ 4, /* field[4] = digits */ 2, /* field[2] = issuer */ 1, /* field[1] = name */ 0, /* field[0] = secret */ 5, /* field[5] = type */ }; static const ProtobufCIntRange migration_payload__otp_parameters__number_ranges[1 + 1] = { { 1, 0 }, { 0, 7 } }; const ProtobufCMessageDescriptor migration_payload__otp_parameters__descriptor = { PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, "MigrationPayload.OtpParameters", "OtpParameters", "MigrationPayload__OtpParameters", "", sizeof(MigrationPayload__OtpParameters), 7, migration_payload__otp_parameters__field_descriptors, migration_payload__otp_parameters__field_indices_by_name, 1, migration_payload__otp_parameters__number_ranges, (ProtobufCMessageInit) migration_payload__otp_parameters__init, NULL,NULL,NULL /* reserved[123] */ }; static const ProtobufCEnumValue migration_payload__algorithm__enum_values_by_number[5] = { { "ALGORITHM_UNSPECIFIED", "MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_UNSPECIFIED", 0 }, { "ALGORITHM_SHA1", "MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_SHA1", 1 }, { "ALGORITHM_SHA256", "MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_SHA256", 2 }, { "ALGORITHM_SHA512", "MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_SHA512", 3 }, { "ALGORITHM_MD5", "MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_MD5", 4 }, }; static const ProtobufCIntRange migration_payload__algorithm__value_ranges[] = { {0, 0},{0, 5} }; static const ProtobufCEnumValueIndex migration_payload__algorithm__enum_values_by_name[5] = { { "ALGORITHM_MD5", 4 }, { "ALGORITHM_SHA1", 1 }, { "ALGORITHM_SHA256", 2 }, { "ALGORITHM_SHA512", 3 }, { "ALGORITHM_UNSPECIFIED", 0 }, }; const ProtobufCEnumDescriptor migration_payload__algorithm__descriptor = { PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, "MigrationPayload.Algorithm", "Algorithm", "MigrationPayload__Algorithm", "", 5, migration_payload__algorithm__enum_values_by_number, 5, migration_payload__algorithm__enum_values_by_name, 1, migration_payload__algorithm__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; static const ProtobufCEnumValue migration_payload__digit_count__enum_values_by_number[3] = { { "DIGIT_COUNT_UNSPECIFIED", "MIGRATION_PAYLOAD__DIGIT_COUNT__DIGIT_COUNT_UNSPECIFIED", 0 }, { "DIGIT_COUNT_SIX", "MIGRATION_PAYLOAD__DIGIT_COUNT__DIGIT_COUNT_SIX", 1 }, { "DIGIT_COUNT_EIGHT", "MIGRATION_PAYLOAD__DIGIT_COUNT__DIGIT_COUNT_EIGHT", 2 }, }; static const ProtobufCIntRange migration_payload__digit_count__value_ranges[] = { {0, 0},{0, 3} }; static const ProtobufCEnumValueIndex migration_payload__digit_count__enum_values_by_name[3] = { { "DIGIT_COUNT_EIGHT", 2 }, { "DIGIT_COUNT_SIX", 1 }, { "DIGIT_COUNT_UNSPECIFIED", 0 }, }; const ProtobufCEnumDescriptor migration_payload__digit_count__descriptor = { PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, "MigrationPayload.DigitCount", "DigitCount", "MigrationPayload__DigitCount", "", 3, migration_payload__digit_count__enum_values_by_number, 3, migration_payload__digit_count__enum_values_by_name, 1, migration_payload__digit_count__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; static const ProtobufCEnumValue migration_payload__otp_type__enum_values_by_number[3] = { { "OTP_TYPE_UNSPECIFIED", "MIGRATION_PAYLOAD__OTP_TYPE__OTP_TYPE_UNSPECIFIED", 0 }, { "OTP_TYPE_HOTP", "MIGRATION_PAYLOAD__OTP_TYPE__OTP_TYPE_HOTP", 1 }, { "OTP_TYPE_TOTP", "MIGRATION_PAYLOAD__OTP_TYPE__OTP_TYPE_TOTP", 2 }, }; static const ProtobufCIntRange migration_payload__otp_type__value_ranges[] = { {0, 0},{0, 3} }; static const ProtobufCEnumValueIndex migration_payload__otp_type__enum_values_by_name[3] = { { "OTP_TYPE_HOTP", 1 }, { "OTP_TYPE_TOTP", 2 }, { "OTP_TYPE_UNSPECIFIED", 0 }, }; const ProtobufCEnumDescriptor migration_payload__otp_type__descriptor = { PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, "MigrationPayload.OtpType", "OtpType", "MigrationPayload__OtpType", "", 3, migration_payload__otp_type__enum_values_by_number, 3, migration_payload__otp_type__enum_values_by_name, 1, migration_payload__otp_type__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; static const ProtobufCFieldDescriptor migration_payload__field_descriptors[5] = { { "otp_parameters", 1, PROTOBUF_C_LABEL_REPEATED, PROTOBUF_C_TYPE_MESSAGE, offsetof(MigrationPayload, n_otp_parameters), offsetof(MigrationPayload, otp_parameters), &migration_payload__otp_parameters__descriptor, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "version", 2, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_INT32, 0, /* quantifier_offset */ offsetof(MigrationPayload, version), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "batch_size", 3, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_INT32, 0, /* quantifier_offset */ offsetof(MigrationPayload, batch_size), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "batch_index", 4, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_INT32, 0, /* quantifier_offset */ offsetof(MigrationPayload, batch_index), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, { "batch_id", 5, PROTOBUF_C_LABEL_NONE, PROTOBUF_C_TYPE_INT32, 0, /* quantifier_offset */ offsetof(MigrationPayload, batch_id), NULL, NULL, 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, }; static const unsigned migration_payload__field_indices_by_name[] = { 4, /* field[4] = batch_id */ 3, /* field[3] = batch_index */ 2, /* field[2] = batch_size */ 0, /* field[0] = otp_parameters */ 1, /* field[1] = version */ }; static const ProtobufCIntRange migration_payload__number_ranges[1 + 1] = { { 1, 0 }, { 0, 5 } }; const ProtobufCMessageDescriptor migration_payload__descriptor = { PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, "MigrationPayload", "MigrationPayload", "MigrationPayload", "", sizeof(MigrationPayload), 5, migration_payload__field_descriptors, migration_payload__field_indices_by_name, 1, migration_payload__number_ranges, (ProtobufCMessageInit) migration_payload__init, NULL,NULL,NULL /* reserved[123] */ }; OTPClient-3.2.1/src/google-migration.pb-c.h000066400000000000000000000113321452112020400203300ustar00rootroot00000000000000/* Generated by the protocol buffer compiler. DO NOT EDIT! */ /* Generated from: data/google-migration.proto */ #ifndef PROTOBUF_C_data_2fgoogle_2dmigration_2eproto__INCLUDED #define PROTOBUF_C_data_2fgoogle_2dmigration_2eproto__INCLUDED #include PROTOBUF_C__BEGIN_DECLS #if PROTOBUF_C_VERSION_NUMBER < 1003000 # error This file was generated by a newer version of protoc-c which is incompatible with your libprotobuf-c headers. Please update your headers. #elif 1004000 < PROTOBUF_C_MIN_COMPILER_VERSION # error This file was generated by an older version of protoc-c which is incompatible with your libprotobuf-c headers. Please regenerate this file with a newer version of protoc-c. #endif typedef struct MigrationPayload MigrationPayload; typedef struct MigrationPayload__OtpParameters MigrationPayload__OtpParameters; /* --- enums --- */ typedef enum _MigrationPayload__Algorithm { MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_UNSPECIFIED = 0, MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_SHA1 = 1, MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_SHA256 = 2, MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_SHA512 = 3, MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_MD5 = 4 PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(MIGRATION_PAYLOAD__ALGORITHM) } MigrationPayload__Algorithm; typedef enum _MigrationPayload__DigitCount { MIGRATION_PAYLOAD__DIGIT_COUNT__DIGIT_COUNT_UNSPECIFIED = 0, MIGRATION_PAYLOAD__DIGIT_COUNT__DIGIT_COUNT_SIX = 1, MIGRATION_PAYLOAD__DIGIT_COUNT__DIGIT_COUNT_EIGHT = 2 PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(MIGRATION_PAYLOAD__DIGIT_COUNT) } MigrationPayload__DigitCount; typedef enum _MigrationPayload__OtpType { MIGRATION_PAYLOAD__OTP_TYPE__OTP_TYPE_UNSPECIFIED = 0, MIGRATION_PAYLOAD__OTP_TYPE__OTP_TYPE_HOTP = 1, MIGRATION_PAYLOAD__OTP_TYPE__OTP_TYPE_TOTP = 2 PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(MIGRATION_PAYLOAD__OTP_TYPE) } MigrationPayload__OtpType; /* --- messages --- */ struct MigrationPayload__OtpParameters { ProtobufCMessage base; ProtobufCBinaryData secret; char *name; char *issuer; MigrationPayload__Algorithm algorithm; MigrationPayload__DigitCount digits; MigrationPayload__OtpType type; int64_t counter; }; #define MIGRATION_PAYLOAD__OTP_PARAMETERS__INIT \ { PROTOBUF_C_MESSAGE_INIT (&migration_payload__otp_parameters__descriptor) \ , {0,NULL}, (char *)protobuf_c_empty_string, (char *)protobuf_c_empty_string, MIGRATION_PAYLOAD__ALGORITHM__ALGORITHM_UNSPECIFIED, MIGRATION_PAYLOAD__DIGIT_COUNT__DIGIT_COUNT_UNSPECIFIED, MIGRATION_PAYLOAD__OTP_TYPE__OTP_TYPE_UNSPECIFIED, 0 } struct MigrationPayload { ProtobufCMessage base; size_t n_otp_parameters; MigrationPayload__OtpParameters **otp_parameters; int32_t version; int32_t batch_size; int32_t batch_index; int32_t batch_id; }; #define MIGRATION_PAYLOAD__INIT \ { PROTOBUF_C_MESSAGE_INIT (&migration_payload__descriptor) \ , 0,NULL, 0, 0, 0, 0 } /* MigrationPayload__OtpParameters methods */ void migration_payload__otp_parameters__init (MigrationPayload__OtpParameters *message); /* MigrationPayload methods */ void migration_payload__init (MigrationPayload *message); size_t migration_payload__get_packed_size (const MigrationPayload *message); size_t migration_payload__pack (const MigrationPayload *message, uint8_t *out); size_t migration_payload__pack_to_buffer (const MigrationPayload *message, ProtobufCBuffer *buffer); MigrationPayload * migration_payload__unpack (ProtobufCAllocator *allocator, size_t len, const uint8_t *data); void migration_payload__free_unpacked (MigrationPayload *message, ProtobufCAllocator *allocator); /* --- per-message closures --- */ typedef void (*MigrationPayload__OtpParameters_Closure) (const MigrationPayload__OtpParameters *message, void *closure_data); typedef void (*MigrationPayload_Closure) (const MigrationPayload *message, void *closure_data); /* --- services --- */ /* --- descriptors --- */ extern const ProtobufCMessageDescriptor migration_payload__descriptor; extern const ProtobufCMessageDescriptor migration_payload__otp_parameters__descriptor; extern const ProtobufCEnumDescriptor migration_payload__algorithm__descriptor; extern const ProtobufCEnumDescriptor migration_payload__digit_count__descriptor; extern const ProtobufCEnumDescriptor migration_payload__otp_type__descriptor; PROTOBUF_C__END_DECLS #endif /* PROTOBUF_C_data_2fgoogle_2dmigration_2eproto__INCLUDED */ OTPClient-3.2.1/src/gquarks.c000066400000000000000000000011061452112020400157130ustar00rootroot00000000000000#include GQuark missing_file_gquark (void) { return g_quark_from_static_string ("missing_file"); } GQuark bad_tag_gquark (void) { return g_quark_from_static_string ("bad_tag"); } GQuark key_deriv_gquark (void) { return g_quark_from_static_string ("key_deriv"); } GQuark file_too_big_gquark (void) { return g_quark_from_static_string ("file_too_big"); } GQuark generic_error_gquark (void) { return g_quark_from_static_string ("generic_error"); } GQuark memlock_error_gquark (void) { return g_quark_from_static_string ("memlock_error"); } OTPClient-3.2.1/src/gquarks.h000066400000000000000000000007631452112020400157300ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS #define MISSING_FILE_CODE 10 #define BAD_TAG_ERRCODE 11 #define KEY_DERIVATION_ERRCODE 12 #define FILE_TOO_BIG 13 #define GENERIC_ERRCODE 14 #define MEMLOCK_ERRCODE 15 GQuark missing_file_gquark (void); GQuark bad_tag_gquark (void); GQuark key_deriv_gquark (void); GQuark file_too_big_gquark (void); GQuark generic_error_gquark (void); GQuark memlock_error_gquark (void); G_END_DECLS OTPClient-3.2.1/src/gui-common.c000066400000000000000000000062171452112020400163200ustar00rootroot00000000000000#include #include #include "message-dialogs.h" #include "add-common.h" #include "common/common.h" void icon_press_cb (GtkEntry *entry, gint position __attribute__((unused)), GdkEventButton *event __attribute__((unused)), gpointer data __attribute__((unused))) { gtk_entry_set_visibility (GTK_ENTRY (entry), !gtk_entry_get_visibility (entry)); } guint get_row_number_from_iter (GtkListStore *list_store, GtkTreeIter iter) { GtkTreePath *path = gtk_tree_model_get_path (GTK_TREE_MODEL(list_store), &iter); gint *row_number = gtk_tree_path_get_indices (path); // starts from 0 guint row = (guint)row_number[0]; gtk_tree_path_free (path); return row; } json_t * build_json_obj (const gchar *type, const gchar *acc_label, const gchar *acc_iss, const gchar *acc_key, guint digits, const gchar *algo, guint period, guint64 ctr) { json_t *obj = json_object (); json_object_set (obj, "type", json_string (type)); json_object_set (obj, "label", json_string (acc_label)); json_object_set (obj, "issuer", json_string (acc_iss)); json_object_set (obj, "secret", json_string (acc_key)); json_object_set (obj, "digits", json_integer (digits)); json_object_set (obj, "algo", json_string (algo)); json_object_set (obj, "secret", json_string (acc_key)); if (g_ascii_strcasecmp (type, "TOTP") == 0) { json_object_set (obj, "period", json_integer (period)); } else { json_object_set (obj, "counter", json_integer (ctr)); } return obj; } void send_ok_cb (GtkWidget *entry, gpointer user_data __attribute__((unused))) { gtk_dialog_response (GTK_DIALOG(gtk_widget_get_toplevel (entry)), GTK_RESPONSE_OK); } gchar * parse_uris_migration (AppData *app_data, const gchar *user_uri, gboolean google_migration) { gchar *return_err_msg = NULL; GSList *otpauth_decoded_uris = NULL; if (google_migration == TRUE) { gint failed = 0; otpauth_decoded_uris = decode_migration_data (user_uri); for (gint i = 0; i < g_slist_length (otpauth_decoded_uris); i++) { gchar *uri = g_slist_nth_data (otpauth_decoded_uris, i); gchar *err_msg = add_data_to_db (uri, app_data); if (err_msg != NULL) { failed++; g_free (err_msg); } } if (failed > 0) { GString *e_msg = g_string_new (NULL); g_string_printf (e_msg, "Failed to add all OTPs. Only %u out of %u were successfully added.", g_slist_length (otpauth_decoded_uris) - failed, g_slist_length (otpauth_decoded_uris)); return_err_msg = g_strdup (e_msg->str); g_string_free (e_msg, TRUE); } g_slist_free_full (otpauth_decoded_uris, g_free); } else { return_err_msg = add_data_to_db (user_uri, app_data); } return return_err_msg; }OTPClient-3.2.1/src/gui-common.h000066400000000000000000000023141452112020400163170ustar00rootroot00000000000000#pragma once #include #include #include "data.h" G_BEGIN_DECLS void icon_press_cb (GtkEntry *entry, gint position, GdkEventButton *event, gpointer data); guint get_row_number_from_iter (GtkListStore *list_store, GtkTreeIter iter); json_t *build_json_obj (const gchar *type, const gchar *acc_label, const gchar *acc_iss, const gchar *acc_key, guint digits, const gchar *algo, guint period, guint64 ctr); void send_ok_cb (GtkWidget *entry, gpointer user_data); gchar *parse_uris_migration (AppData *app_data, const gchar *user_uri, gboolean google_migration); G_END_DECLS OTPClient-3.2.1/src/imports.c000066400000000000000000000123121452112020400157340ustar00rootroot00000000000000#include #include #include #include "imports.h" #include "password-cb.h" #include "message-dialogs.h" #include "gquarks.h" #include "common/common.h" #include "gui-common.h" #include "db-misc.h" #include "common/get-providers-data.h" static gboolean parse_data_and_update_db (AppData *app_data, const gchar *filename, const gchar *action_name); void select_file_cb (GSimpleAction *simple, GVariant *parameter __attribute__((unused)), gpointer user_data) { const gchar *action_name = g_action_get_name (G_ACTION(simple)); AppData *app_data = (AppData *)user_data; GtkFileChooserNative *dialog = gtk_file_chooser_native_new ("Open File", GTK_WINDOW(app_data->main_window), GTK_FILE_CHOOSER_ACTION_OPEN, "Open", "Cancel"); gint res = gtk_native_dialog_run (GTK_NATIVE_DIALOG(dialog)); if (res == GTK_RESPONSE_ACCEPT) { GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog); gchar *filename = gtk_file_chooser_get_filename (chooser); parse_data_and_update_db (app_data, filename, action_name); g_free (filename); } g_object_unref (dialog); } gchar * update_db_from_otps (GSList *otps, AppData *app_data) { json_t *obj; guint list_len = g_slist_length (otps); for (guint i = 0; i < list_len; i++) { otp_t *otp = g_slist_nth_data (otps, i); obj = build_json_obj (otp->type, otp->account_name, otp->issuer, otp->secret, otp->digits, otp->algo, otp->period, otp->counter); guint hash = json_object_get_hash (obj); if (g_slist_find_custom (app_data->db_data->objects_hash, GUINT_TO_POINTER(hash), check_duplicate) == NULL) { app_data->db_data->objects_hash = g_slist_append (app_data->db_data->objects_hash, g_memdupX (&hash, sizeof (guint))); app_data->db_data->data_to_add = g_slist_append (app_data->db_data->data_to_add, obj); } else { g_print ("[INFO] Duplicate element not added\n"); } } GError *err = NULL; update_and_reload_db (app_data, app_data->db_data, TRUE, &err); if (err != NULL && !g_error_matches (err, missing_file_gquark (), MISSING_FILE_CODE)) { return g_strdup (err->message); } return NULL; } void free_otps_gslist (GSList *otps, guint list_len) { otp_t *otp_data; for (guint i = 0; i < list_len; i++) { otp_data = g_slist_nth_data (otps, i); g_free (otp_data->type); g_free (otp_data->algo); g_free (otp_data->account_name); g_free (otp_data->issuer); gcry_free (otp_data->secret); } g_slist_free_full (otps, g_free); } static gboolean parse_data_and_update_db (AppData *app_data, const gchar *filename, const gchar *action_name) { GError *err = NULL; GSList *content = NULL; gchar *pwd = NULL; if (g_strcmp0 (action_name, ANDOTP_IMPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0) { pwd = prompt_for_password (app_data, NULL, action_name, FALSE); if (pwd == NULL) { return FALSE; } } if (g_strcmp0 (action_name, ANDOTP_IMPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, ANDOTP_IMPORT_PLAIN_ACTION_NAME) == 0) { content = get_andotp_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, g_strcmp0 (action_name, ANDOTP_IMPORT_ACTION_NAME) == 0 ? TRUE : FALSE , &err); } else if (g_strcmp0 (action_name, FREEOTPPLUS_IMPORT_ACTION_NAME) == 0) { content = get_freeotpplus_data (filename, &err); } else if (g_strcmp0 (action_name, AEGIS_IMPORT_ACTION_NAME) == 0 || g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0) { content = get_aegis_data (filename, pwd, app_data->db_data->max_file_size_from_memlock, g_strcmp0 (action_name, AEGIS_IMPORT_ENC_ACTION_NAME) == 0 ? TRUE : FALSE , &err); } if (content == NULL) { const gchar *msg = "An error occurred while importing, so nothing has been added to the database."; gchar *msg_with_err = NULL; if (err != NULL) { msg_with_err = g_strconcat (msg, " The error is:\n", err->message, NULL); } show_message_dialog (app_data->main_window, err == NULL ? msg : msg_with_err, GTK_MESSAGE_ERROR); g_free (msg_with_err); if (err != NULL){ g_clear_error (&err); } if (pwd != NULL) { gcry_free (pwd); } return FALSE; } gchar *err_msg = update_db_from_otps (content, app_data); if (err_msg != NULL) { show_message_dialog (app_data->main_window, err_msg, GTK_MESSAGE_ERROR); g_free (err_msg); if (pwd != NULL) { gcry_free (pwd); } return FALSE; } if (pwd != NULL) { gcry_free (pwd); } free_otps_gslist (content, g_slist_length (content)); return TRUE; } OTPClient-3.2.1/src/imports.h000066400000000000000000000022361452112020400157450ustar00rootroot00000000000000#pragma once #include #include "data.h" G_BEGIN_DECLS #define ANDOTP_IMPORT_ACTION_NAME "import_andotp" #define ANDOTP_IMPORT_PLAIN_ACTION_NAME "import_andotp_plain" #define FREEOTPPLUS_IMPORT_ACTION_NAME "import_freeotpplus" #define AEGIS_IMPORT_ACTION_NAME "import_aegis" #define AEGIS_IMPORT_ENC_ACTION_NAME "import_aegis_enc" #define GOOGLE_MIGRATION_FILE_ACTION_NAME "import_google_qr_file" #define GOOGLE_MIGRATION_WEBCAM_ACTION_NAME "import_google_qr_webcam" typedef struct otp_object_t { gchar *type; gchar *algo; guint32 digits; union { guint32 period; guint64 counter; }; gchar *account_name; gchar *issuer; gchar *secret; } otp_t; void select_file_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); gchar *update_db_from_otps (GSList *otps, AppData *app_data); void free_otps_gslist (GSList *otps, guint list_len); G_END_DECLS OTPClient-3.2.1/src/liststore-misc.c000066400000000000000000000137051452112020400172270ustar00rootroot00000000000000#include #include #include #include "treeview.h" #include "liststore-misc.h" #include "gquarks.h" #include "common/common.h" typedef struct otp_data_t { gchar *type; gchar *secret; gchar *algo; gint digits; gint period; gint64 counter; gboolean steam; } OtpData; static void set_otp_data (OtpData *otp_data, AppData *app_data, gint row_db_pos); static gboolean foreach_func_update_otps (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data); static void clean_otp_data (OtpData *otp_data); gboolean traverse_liststore (gpointer user_data) { AppData *app_data = (AppData *)user_data; gtk_tree_model_foreach (GTK_TREE_MODEL(gtk_tree_view_get_model (app_data->tree_view)), foreach_func_update_otps, app_data); return TRUE; } void set_otp (GtkListStore *list_store, GtkTreeIter iter, AppData *app_data) { OtpData *otp_data = g_new0 (OtpData, 1); gint row_db_pos; gtk_tree_model_get (GTK_TREE_MODEL(list_store), &iter, COLUMN_POSITION_IN_DB, &row_db_pos, -1); set_otp_data (otp_data, app_data, row_db_pos); gint algo = get_algo_int_from_str (otp_data->algo); cotp_error_t otp_err; gchar *otp; if (g_ascii_strcasecmp (otp_data->type, "TOTP") == 0) { if (otp_data->steam) { otp = get_steam_totp (otp_data->secret, otp_data->period, &otp_err); } else { otp = get_totp (otp_data->secret, otp_data->digits, otp_data->period, algo, &otp_err); } } else { // clean previous HOTP info g_free (app_data->db_data->last_hotp); g_date_time_unref (app_data->db_data->last_hotp_update); otp = get_hotp (otp_data->secret, otp_data->counter, otp_data->digits, algo, &otp_err); app_data->db_data->last_hotp = g_strdup (otp); app_data->db_data->last_hotp_update = g_date_time_new_now_local (); } if (otp_err == INVALID_B32_INPUT) { clean_otp_data (otp_data); return; } gtk_list_store_set (list_store, &iter, COLUMN_OTP, otp, -1); app_data->last_user_activity = g_date_time_new_now_local (); g_free (otp); clean_otp_data (otp_data); } static gboolean foreach_func_update_otps (GtkTreeModel *model, GtkTreePath *path __attribute__((unused)), GtkTreeIter *iter, gpointer user_data) { AppData *app_data = (AppData *)user_data; gchar *otp_type, *otp; guint validity, period; gboolean only_a_minute_left, already_updated_once; gtk_tree_model_get (model, iter, COLUMN_TYPE, &otp_type, COLUMN_OTP, &otp, COLUMN_VALIDITY, &validity, COLUMN_PERIOD, &period, COLUMN_UPDATED, &already_updated_once, COLUMN_LESS_THAN_A_MINUTE, &only_a_minute_left, -1); if (otp != NULL && g_utf8_strlen (otp, -1) > 4 && g_ascii_strcasecmp (otp_type, "TOTP") == 0) { gboolean short_countdown = (period <= 60 || only_a_minute_left) ? TRUE : FALSE; guint remaining_seconds = (!short_countdown ? 119 : 59) - g_date_time_get_second (g_date_time_new_now_local()); guint token_validity = remaining_seconds % period; if (remaining_seconds % period == 60) { short_countdown = TRUE; } if ((remaining_seconds % period) == (period - 1)) { if ((app_data->show_next_otp) && (already_updated_once == FALSE)) { already_updated_once = TRUE; set_otp (GTK_LIST_STORE (model), *iter, app_data); } else { short_countdown = FALSE; already_updated_once = FALSE; token_validity = 0; gtk_list_store_set (GTK_LIST_STORE (model), iter, COLUMN_OTP, "", -1); } } gtk_list_store_set (GTK_LIST_STORE (model), iter, COLUMN_VALIDITY, token_validity, COLUMN_UPDATED, already_updated_once, COLUMN_LESS_THAN_A_MINUTE, short_countdown, -1); } g_free (otp_type); g_free (otp); // do not stop walking the store, check next row return FALSE; } static void set_otp_data (OtpData *otp_data, AppData *app_data, gint row_db_pos) { json_t *obj = json_array_get (app_data->db_data->json_data, row_db_pos); otp_data->type = g_strdup (json_string_value (json_object_get (obj, "type"))); otp_data->secret = secure_strdup (json_string_value (json_object_get (obj, "secret"))); otp_data->algo = g_strdup (json_string_value (json_object_get (obj, "algo"))); otp_data->digits = (gint)json_integer_value (json_object_get (obj, "digits")); const gchar *issuer = json_string_value (json_object_get (obj, "issuer")); otp_data->steam = ((issuer != NULL && g_ascii_strcasecmp (issuer, "steam") == 0) ? TRUE : FALSE); if (json_object_get (obj, "counter") != NULL) { GError *err = NULL; otp_data->counter = json_integer_value (json_object_get (obj, "counter")); // every time HOTP is accessed, counter must be increased json_object_set (obj, "counter", json_integer (otp_data->counter + 1)); update_and_reload_db (app_data, app_data->db_data, FALSE, &err); if (err != NULL && !g_error_matches (err, missing_file_gquark (), MISSING_FILE_CODE)) { g_printerr ("%s\n", err->message); } } else { otp_data->period = (gint)json_integer_value (json_object_get (obj, "period")); } } static void clean_otp_data (OtpData *otp_data) { g_free (otp_data->type); gcry_free (otp_data->secret); g_free (otp_data->algo); g_free (otp_data); } OTPClient-3.2.1/src/liststore-misc.h000066400000000000000000000004361452112020400172310ustar00rootroot00000000000000#pragma once G_BEGIN_DECLS #include "db-misc.h" gboolean traverse_liststore (gpointer user_data); void set_otp (GtkListStore *list_store, GtkTreeIter iter, AppData *app_data); G_END_DECLS OTPClient-3.2.1/src/lock-app.c000066400000000000000000000110221452112020400157420ustar00rootroot00000000000000#include #include #include #include "data.h" #include "get-builder.h" #include "gui-common.h" #include "message-dialogs.h" #include "otpclient.h" #include "lock-app.h" void lock_app (GtkWidget *w __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; app_data->app_locked = TRUE; g_signal_emit_by_name (app_data->tree_view, "hide-all-otps"); gtk_widget_hide (GTK_WIDGET(app_data->tree_view)); GtkBuilder *builder = get_builder_from_partial_path (UI_PARTIAL_PATH); GtkWidget *dialog = GTK_WIDGET(gtk_builder_get_object (builder, "unlock_pwd_diag_id")); GtkWidget *pwd_entry = GTK_WIDGET(gtk_builder_get_object (builder, "unlock_entry_id")); g_signal_connect (pwd_entry, "icon-press", G_CALLBACK (icon_press_cb), NULL); g_signal_connect (pwd_entry, "activate", G_CALLBACK (send_ok_cb), NULL); gtk_window_set_transient_for (GTK_WINDOW(dialog), GTK_WINDOW(app_data->main_window)); gtk_widget_show_all (dialog); gint ret; gboolean retry = FALSE; do { ret = gtk_dialog_run (GTK_DIALOG(dialog)); if (ret == GTK_RESPONSE_OK) { if (g_strcmp0 (app_data->db_data->key, gtk_entry_get_text (GTK_ENTRY(pwd_entry))) != 0) { show_message_dialog (dialog, "The password is wrong, please try again.", GTK_MESSAGE_ERROR); gtk_entry_set_text (GTK_ENTRY(pwd_entry), ""); retry = TRUE; } else { retry = FALSE; app_data->app_locked = FALSE; app_data->last_user_activity = g_date_time_new_now_local (); app_data->source_id_last_activity = g_timeout_add_seconds (1, check_inactivity, app_data); gtk_widget_destroy (dialog); gtk_widget_show (GTK_WIDGET(app_data->tree_view)); g_object_unref (builder); } } else { gtk_widget_destroy (dialog); g_object_unref (builder); GtkApplication *app = gtk_window_get_application (GTK_WINDOW (app_data->main_window)); destroy_cb (app_data->main_window, app_data); g_application_quit (G_APPLICATION(app)); } } while (ret == GTK_RESPONSE_OK && retry == TRUE); } static void signal_triggered_cb (GDBusConnection *connection __attribute__((unused)), const gchar *sender_name __attribute__((unused)), const gchar *object_path __attribute__((unused)), const gchar *interface_name __attribute__((unused)), const gchar *signal_name __attribute__((unused)), GVariant *parameters, gpointer user_data) { AppData *app_data = (AppData *)user_data; gboolean is_screen_locked; g_variant_get (parameters, "(b)", &is_screen_locked); if (is_screen_locked == TRUE && app_data->app_locked == FALSE && app_data->auto_lock == TRUE) { lock_app (NULL, app_data); } } gboolean check_inactivity (gpointer user_data) { AppData *app_data = (AppData *)user_data; if (app_data->inactivity_timeout > 0 && app_data->app_locked == FALSE) { GDateTime *now = g_date_time_new_now_local (); GTimeSpan diff = g_date_time_difference (now, app_data->last_user_activity); if (diff >= (G_USEC_PER_SEC * (GTimeSpan)app_data->inactivity_timeout)) { g_signal_emit_by_name (app_data->main_window, "lock-app"); g_date_time_unref (now); return FALSE; } g_date_time_unref (now); } return TRUE; } void setup_dbus_listener (AppData *app_data) { g_signal_connect (app_data->main_window, "lock-app", G_CALLBACK(lock_app), app_data); const gchar *paths[] = { "/org/cinnamon/ScreenSaver", "/org/freedesktop/ScreenSaver", "/org/gnome/ScreenSaver", "/com/canonical/Unity/Session" }; const gchar *interfaces[] = { "org.cinnamon.ScreenSaver", "org.freedesktop.ScreenSaver", "org.gnome.ScreenSaver", "com.canonical.Unity.Session" }; const gchar *signal_names[] = { "ActiveChanged", "ActiveChanged", "ActiveChanged", "Locked" }; app_data->connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); for (guint i = 0; i < DBUS_SERVICES; i++) { app_data->subscription_ids[i] = g_dbus_connection_signal_subscribe (app_data->connection, interfaces[i], interfaces[i], signal_names[i], paths[i], NULL, G_DBUS_SIGNAL_FLAGS_NONE, signal_triggered_cb, app_data, NULL); } } OTPClient-3.2.1/src/lock-app.h000066400000000000000000000004311452112020400157510ustar00rootroot00000000000000#pragma once #include "data.h" G_BEGIN_DECLS void lock_app (GtkWidget *w, gpointer user_data); void setup_dbus_listener (AppData *app_data); gboolean check_inactivity (gpointer user_data); G_END_DECLS OTPClient-3.2.1/src/main.c000066400000000000000000000013661452112020400151720ustar00rootroot00000000000000#include #include #include "otpclient.h" #include "version.h" gint main (gint argc, gchar **argv) { bindtextdomain (GETTEXT_PACKAGE, LOCALE_DIR); bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); textdomain (GETTEXT_PACKAGE); GApplicationFlags flags; #if GLIB_CHECK_VERSION(2, 74, 0) flags = G_APPLICATION_DEFAULT_FLAGS; #else flags = G_APPLICATION_FLAGS_NONE; #endif GtkApplication *app = gtk_application_new ("com.github.paolostivanin.OTPClient", flags); g_set_application_name (PROJECT_NAME); g_signal_connect (app, "activate", G_CALLBACK (activate), NULL); gint status = g_application_run (G_APPLICATION (app), argc, argv); g_object_unref (app); return status; }OTPClient-3.2.1/src/manual-add-cb.c000066400000000000000000000114141452112020400166260ustar00rootroot00000000000000#include #include "db-misc.h" #include "gui-common.h" #include "manual-add-cb.h" #include "gquarks.h" #include "message-dialogs.h" #include "get-builder.h" static void changed_otp_cb (GtkWidget *cb, gpointer user_data); static void steam_toggled_cb (GtkWidget * __attribute__((unused)), gpointer user_data); void manual_add_cb (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; Widgets *widgets = g_new0 (Widgets, 1); GtkBuilder *builder = get_builder_from_partial_path (UI_PARTIAL_PATH); widgets->dialog = GTK_WIDGET(gtk_builder_get_object (builder, "manual_add_diag_id")); widgets->otp_cb = GTK_WIDGET(gtk_builder_get_object (builder, "otp_combotext_id")); widgets->algo_cb = GTK_WIDGET(gtk_builder_get_object (builder, "algo_combotext_id")); widgets->steam_ck = GTK_WIDGET(gtk_builder_get_object (builder, "steam_ck_btn")); widgets->label_entry = GTK_WIDGET(gtk_builder_get_object (builder, "manual_diag_label_entry_id")); widgets->iss_entry = GTK_WIDGET(gtk_builder_get_object (builder, "manual_diag_issuer_entry_id")); widgets->sec_entry = GTK_WIDGET(gtk_builder_get_object (builder, "manual_diag_secret_entry_id")); widgets->digits_entry = GTK_WIDGET(gtk_builder_get_object (builder, "digits_entry_manual_diag")); widgets->period_entry = GTK_WIDGET(gtk_builder_get_object (builder, "period_entry_manual_diag")); widgets->counter_entry = GTK_WIDGET(gtk_builder_get_object (builder, "counter_entry_manual_diag")); gtk_widget_set_sensitive (widgets->counter_entry, FALSE); // by default TOTP is selected, so we don't need counter_cb gtk_window_set_transient_for (GTK_WINDOW(widgets->dialog), GTK_WINDOW(app_data->main_window)); g_signal_connect (widgets->sec_entry, "icon-press", G_CALLBACK(icon_press_cb), NULL); g_signal_connect (widgets->otp_cb, "changed", G_CALLBACK(changed_otp_cb), widgets); g_signal_connect (widgets->steam_ck, "toggled", G_CALLBACK(steam_toggled_cb), widgets); GError *err = NULL; gboolean retry = TRUE; gint result; do { result = gtk_dialog_run (GTK_DIALOG(widgets->dialog)); if (result == GTK_RESPONSE_OK) { if (parse_user_data (widgets, app_data->db_data)) { update_and_reload_db (app_data, app_data->db_data, TRUE, &err); if (err != NULL && !g_error_matches (err, missing_file_gquark (), MISSING_FILE_CODE)) { show_message_dialog (app_data->main_window, err->message, GTK_MESSAGE_ERROR); } retry = FALSE; } } } while (result == GTK_RESPONSE_OK && retry == TRUE); gtk_widget_destroy (widgets->dialog); g_free (widgets); g_object_unref (builder); } void manual_add_cb_shortcut (GtkWidget *w __attribute__((unused)), gpointer user_data) { manual_add_cb (NULL, NULL, user_data); } static void changed_otp_cb (GtkWidget *cb, gpointer user_data) { Widgets *widgets = (Widgets *)user_data; // id 0 (FALSE) is totp, id 1 (TRUE) is hotp gtk_widget_set_sensitive (widgets->counter_entry, gtk_combo_box_get_active (GTK_COMBO_BOX(cb))); gtk_widget_set_sensitive (widgets->period_entry, !gtk_combo_box_get_active (GTK_COMBO_BOX(cb))); } static void steam_toggled_cb (GtkWidget *ck_btn __attribute__((unused)), gpointer user_data) { Widgets *widgets = (Widgets *)user_data; gboolean button_toggled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(widgets->steam_ck)); gtk_widget_set_sensitive (widgets->otp_cb, !button_toggled); gtk_widget_set_sensitive (widgets->algo_cb, !button_toggled); gtk_widget_set_sensitive (widgets->digits_entry, !button_toggled); gtk_widget_set_sensitive (widgets->period_entry, !button_toggled); gtk_widget_set_sensitive (widgets->counter_entry, !button_toggled); g_object_set (widgets->iss_entry, "editable", !button_toggled, NULL); if (button_toggled) { gtk_combo_box_set_active (GTK_COMBO_BOX(widgets->otp_cb), 0); // TOTP gtk_combo_box_set_active (GTK_COMBO_BOX(widgets->algo_cb), 0); // SHA1 gtk_entry_set_text (GTK_ENTRY(widgets->iss_entry), "Steam"); gtk_entry_set_text (GTK_ENTRY(widgets->period_entry), "30"); gtk_entry_set_text (GTK_ENTRY(widgets->digits_entry), "5"); } else { gtk_entry_set_text (GTK_ENTRY(widgets->iss_entry), ""); gtk_entry_set_text (GTK_ENTRY(widgets->digits_entry), ""); gtk_entry_set_text (GTK_ENTRY(widgets->period_entry), ""); gtk_entry_set_text (GTK_ENTRY(widgets->counter_entry), ""); } } OTPClient-3.2.1/src/manual-add-cb.h000066400000000000000000000013611452112020400166330ustar00rootroot00000000000000#pragma once G_BEGIN_DECLS typedef struct widgets_t { GtkWidget *dialog; GtkWidget *otp_cb; GtkWidget *algo_cb; GtkWidget *steam_ck; GtkWidget *label_entry; GtkWidget *iss_entry; GtkWidget *sec_entry; GtkWidget *digits_entry; GtkWidget *period_entry; GtkWidget *counter_entry; } Widgets; void manual_add_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void manual_add_cb_shortcut (GtkWidget *w, gpointer user_data); gboolean parse_user_data (Widgets *widgets, DatabaseData *db_data); G_END_DECLSOTPClient-3.2.1/src/message-dialogs.c000066400000000000000000000032221452112020400173030ustar00rootroot00000000000000#include void show_message_dialog (GtkWidget *parent, const gchar *message, GtkMessageType message_type) { static GtkWidget *dialog = NULL; dialog = gtk_message_dialog_new (parent == NULL ? NULL : GTK_WINDOW(parent), GTK_DIALOG_MODAL, message_type, GTK_BUTTONS_OK, "%s", message); gtk_message_dialog_set_markup (GTK_MESSAGE_DIALOG(dialog), message); gtk_dialog_run (GTK_DIALOG(dialog)); gtk_widget_destroy (dialog); } gboolean get_confirmation_from_dialog (GtkWidget *parent, const gchar *message) { static GtkWidget *dialog = NULL; gboolean confirm; dialog = gtk_dialog_new_with_buttons ("Confirm", GTK_WINDOW (parent), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "OK", GTK_RESPONSE_OK, "Cancel", GTK_RESPONSE_CANCEL, NULL); gtk_container_set_border_width (GTK_CONTAINER (dialog), 5); GtkWidget *content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog)); GtkWidget *label = gtk_label_new (NULL); gtk_label_set_markup (GTK_LABEL (label), message); gtk_label_set_justify (GTK_LABEL (label), GTK_JUSTIFY_CENTER); gtk_container_add (GTK_CONTAINER (content_area), label); gtk_widget_show_all (dialog); gint result = gtk_dialog_run (GTK_DIALOG (dialog)); switch (result) { case GTK_RESPONSE_OK: confirm = TRUE; break; case GTK_RESPONSE_CANCEL: default: confirm = FALSE; break; } gtk_widget_destroy (dialog); return confirm; } OTPClient-3.2.1/src/message-dialogs.h000066400000000000000000000005741452112020400173170ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS void show_message_dialog (GtkWidget *parent, const gchar *message, GtkMessageType message_type); gboolean get_confirmation_from_dialog (GtkWidget *parent, const gchar *message); G_END_DECLS OTPClient-3.2.1/src/new-db-cb.c000066400000000000000000000052671452112020400160100ustar00rootroot00000000000000#include #include #include #include "data.h" #include "db-misc.h" #include "message-dialogs.h" #include "password-cb.h" #include "db-actions.h" #include "secret-schema.h" void new_db_cb (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; GtkWidget *newdb_diag = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "newdb_diag_id")); GtkWidget *newdb_entry = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "newdb_entry_id")); g_object_set_data (G_OBJECT(newdb_entry), "action", GINT_TO_POINTER(ACTION_SAVE)); g_signal_connect (newdb_entry, "icon-press", G_CALLBACK (select_file_icon_pressed_cb), app_data); GString *new_db_path_with_suffix; gint result = gtk_dialog_run (GTK_DIALOG (newdb_diag)); switch (result) { case GTK_RESPONSE_OK: new_db_path_with_suffix = g_string_new (gtk_entry_get_text (GTK_ENTRY(newdb_entry))); g_string_append (new_db_path_with_suffix, ".enc"); if (g_file_test (new_db_path_with_suffix->str, G_FILE_TEST_IS_REGULAR) || g_file_test (new_db_path_with_suffix->str, G_FILE_TEST_IS_SYMLINK)) { show_message_dialog (app_data->main_window, "Selected file already exists, please choose another filename.", GTK_MESSAGE_ERROR); } else { g_free (app_data->db_data->db_path); app_data->db_data->db_path = g_strdup (new_db_path_with_suffix->str); update_cfg_file (app_data); gcry_free (app_data->db_data->key); app_data->db_data->key = prompt_for_password (app_data, NULL, NULL, FALSE); secret_password_store (OTPCLIENT_SCHEMA, SECRET_COLLECTION_DEFAULT, "main_pwd", app_data->db_data->key, NULL, on_password_stored, NULL, "string", "main_pwd", NULL); GError *err = NULL; write_db_to_disk (app_data->db_data, &err); if (err != NULL) { show_message_dialog (app_data->main_window, err->message, GTK_MESSAGE_ERROR); g_clear_error (&err); } else { load_new_db (app_data, &err); if (err != NULL) { show_message_dialog (app_data->main_window, err->message, GTK_MESSAGE_ERROR); g_clear_error (&err); } } } g_string_free (new_db_path_with_suffix, TRUE); break; case GTK_RESPONSE_CANCEL: default: break; } gtk_widget_destroy (newdb_diag); }OTPClient-3.2.1/src/new-db-cb.h000066400000000000000000000002741452112020400160060ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS void new_db_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); G_END_DECLS OTPClient-3.2.1/src/otpclient.h000066400000000000000000000016621452112020400162530ustar00rootroot00000000000000#pragma once #include "treeview.h" G_BEGIN_DECLS #define HOTP_RATE_LIMIT_IN_SEC 3 #define NOTIFICATION_ID "otp-copied" void activate (GtkApplication *app, gpointer user_data); gboolean change_file (AppData *app_data); void add_qr_from_file (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void add_qr_from_clipboard (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void about_diag_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void destroy_cb (GtkWidget *window, gpointer user_data); G_END_DECLS OTPClient-3.2.1/src/parse-data.c000066400000000000000000000157171452112020400162740ustar00rootroot00000000000000#include #include #include #include #include "db-misc.h" #include "manual-add-cb.h" #include "gquarks.h" #include "message-dialogs.h" #include "gui-common.h" #include "common/common.h" static gboolean is_input_valid (GtkWidget *dialog, const gchar *acc_label, const gchar *acc_iss, const gchar *secret, const gchar *digits, const gchar *period, gboolean period_active, const gchar *counter, gboolean counter_active); static gboolean str_is_only_num_or_alpha (const gchar *string); static gboolean str_is_only_num (const gchar *string); static json_t *get_json_obj (Widgets *widgets, const gchar *acc_label, const gchar *acc_iss, const gchar *acc_key, const gchar *digits, const gchar *period, const gchar *counter); gboolean parse_user_data (Widgets *widgets, DatabaseData *db_data) { json_t *obj; const gchar *acc_label = gtk_entry_get_text (GTK_ENTRY (widgets->label_entry)); const gchar *acc_iss = gtk_entry_get_text (GTK_ENTRY (widgets->iss_entry)); const gchar *acc_key = gtk_entry_get_text (GTK_ENTRY (widgets->sec_entry)); const gchar *digits = gtk_entry_get_text (GTK_ENTRY (widgets->digits_entry)); const gchar *period = gtk_entry_get_text (GTK_ENTRY (widgets->period_entry)); const gchar *counter = gtk_entry_get_text (GTK_ENTRY (widgets->counter_entry)); gboolean period_active = gtk_widget_get_sensitive (widgets->period_entry); gboolean counter_active = gtk_widget_get_sensitive (widgets->counter_entry); gchar *acc_key_trimmed = g_trim_whitespace (acc_key); if (is_input_valid (widgets->dialog, acc_label, acc_iss, acc_key_trimmed, digits, period, period_active, counter, counter_active)) { obj = get_json_obj (widgets, acc_label, acc_iss, acc_key_trimmed, digits, period, counter); guint32 hash = json_object_get_hash (obj); if (g_slist_find_custom (db_data->objects_hash, GUINT_TO_POINTER (hash), check_duplicate) == NULL) { db_data->objects_hash = g_slist_append (db_data->objects_hash, g_memdupX (&hash, sizeof (guint))); db_data->data_to_add = g_slist_append (db_data->data_to_add, obj); } else { g_print ("[INFO] Duplicate element not added\n"); } } else { gcry_free (acc_key_trimmed); return FALSE; } gcry_free (acc_key_trimmed); return TRUE; } static gboolean is_input_valid (GtkWidget *dialog, const gchar *acc_label, const gchar *acc_iss, const gchar *secret, const gchar *digits, const gchar *period, gboolean period_active, const gchar *counter, gboolean counter_active) { if (g_utf8_strlen (acc_label, -1) == 0 || g_utf8_strlen (secret, -1) == 0) { show_message_dialog (dialog, "Label and/or secret can't be empty", GTK_MESSAGE_ERROR); return FALSE; } if (!g_str_is_ascii (acc_label) || !g_str_is_ascii (acc_iss)) { gchar *msg = g_strconcat ("Only ASCII characters are supported. Entry with label '", acc_label, "' will not be added.", NULL); show_message_dialog (dialog, msg, GTK_MESSAGE_ERROR); g_free (msg); return FALSE; } if (!str_is_only_num_or_alpha (secret)) { gchar *msg = g_strconcat ("Secret can contain only characters from the english alphabet and digits. Entry with label '", acc_label, "' will not be added.", NULL); show_message_dialog (dialog, msg, GTK_MESSAGE_ERROR); g_free (msg); return FALSE; } if (!str_is_only_num (digits) || g_ascii_strtoll (digits, NULL, 10) < 4 || g_ascii_strtoll (digits, NULL, 10) > 10) { gchar *msg = g_strconcat ("The digits entry should contain only digits and the value should be between 4 and 10 inclusive.\n" "Entry with label '", acc_label, "' will not be added.", NULL); show_message_dialog (dialog, msg, GTK_MESSAGE_ERROR); g_free (msg); return FALSE; } if (period_active && (!str_is_only_num (period) || g_ascii_strtoll (period, NULL, 10) < 10 || g_ascii_strtoll (period, NULL, 10) > 120)) { gchar *msg = g_strconcat ("The period entry should contain only digits and the value should be between 10 and 120 (inclusive).\n" "Entry with label '", acc_label, "' will not be added.", NULL); show_message_dialog (dialog, msg, GTK_MESSAGE_ERROR); g_free (msg); return FALSE; } if (counter_active && (!str_is_only_num (counter) || g_ascii_strtoll (counter, NULL, 10) < 1 || g_ascii_strtoll (counter, NULL, 10) == G_MAXINT64)) { gchar *msg = g_strconcat ("The counter entry should contain only digits and the value should be between 1 and G_MAXINT64-1 (inclusive).\n" "Entry with label '", acc_label, "' will not be added.", NULL); show_message_dialog (dialog, msg, GTK_MESSAGE_ERROR); g_free (msg); return FALSE; } return TRUE; } static gboolean str_is_only_num_or_alpha (const gchar *string) { size_t s_len = g_utf8_strlen (string, -1); for (gint i = 0; i < s_len; i++) { if (!g_ascii_isalnum (string[i])) { return FALSE; } } return TRUE; } static gboolean str_is_only_num (const gchar *string) { size_t s_len = g_utf8_strlen (string, -1); for (gint i = 0; i < s_len; i++) { if (!g_ascii_isdigit (string[i])) { return FALSE; } } return TRUE; } static json_t * get_json_obj (Widgets *widgets, const gchar *acc_label, const gchar *acc_iss, const gchar *acc_key, const gchar *digits, const gchar *period, const gchar *counter) { gchar *type = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (widgets->otp_cb)); gchar *algo = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (widgets->algo_cb)); gint digits_int = (gint)g_ascii_strtoll (digits, NULL, 10); gint period_int = (gint)g_ascii_strtoll (period, NULL, 10); gint64 ctr = g_ascii_strtoll (counter, NULL, 10); json_t *jn = build_json_obj (type, acc_label, acc_iss, acc_key, digits_int, algo, period_int, ctr); g_free (type); g_free (algo); return jn; } OTPClient-3.2.1/src/parse-uri.c000066400000000000000000000164511452112020400161560ustar00rootroot00000000000000#include #include "imports.h" #include "common/common.h" #include "gui-common.h" static void parse_uri (const gchar *uri, GSList **otps); static void parse_parameters (const gchar *modified_uri, otp_t *otp); void set_otps_from_uris (const gchar *otpauth_uris, GSList **otps) { gchar **uris = g_strsplit (otpauth_uris, "\n", -1); guint i = 0, uris_len = g_strv_length (uris); gchar *haystack = NULL; if (uris_len > 0) { for (; i < uris_len; i++) { haystack = g_strrstr (uris[i], "otpauth"); if (haystack != NULL) { parse_uri (haystack, otps); } } } g_strfreev (uris); } gchar * get_otpauth_uri (AppData *app_data, json_t *obj) { gchar *constructed_label = NULL; json_t *db_obj = NULL; if (app_data == NULL) { db_obj = obj; } else { GtkTreeModel *model = gtk_tree_view_get_model (app_data->tree_view); GtkListStore *list_store = GTK_LIST_STORE(model); GtkTreeIter iter; if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (app_data->tree_view), &model, &iter) == FALSE) { return NULL; } guint row_number = get_row_number_from_iter (list_store, iter); db_obj = json_array_get (app_data->db_data->json_data, row_number); } GString *uri = g_string_new (NULL); g_string_append (uri, "otpauth://"); const gchar *issuer = json_string_value (json_object_get (db_obj, "issuer")); if (issuer != NULL && g_ascii_strcasecmp (issuer, "steam") == 0) { g_string_append (uri, "totp/"); constructed_label = g_strconcat ("Steam:", json_string_value (json_object_get (db_obj, "label")), NULL); } else { g_string_append (uri, g_utf8_strdown (json_string_value (json_object_get (db_obj, "type")), -1)); g_string_append (uri, "/"); if (issuer != NULL && g_utf8_strlen (issuer, -1) > 0) { constructed_label = g_strconcat (json_string_value (json_object_get (db_obj, "issuer")), ":", json_string_value (json_object_get (db_obj, "label")), NULL); } else { constructed_label = g_strdup (json_string_value (json_object_get (db_obj, "label"))); } } gchar *escaped_label = g_uri_escape_string (constructed_label, NULL, FALSE); g_string_append (uri, escaped_label); g_string_append (uri, "?secret="); g_string_append (uri, json_string_value (json_object_get (db_obj, "secret"))); if (issuer != NULL && g_ascii_strcasecmp (issuer, "steam") == 0) { g_string_append (uri, "&issuer=Steam"); } if (issuer != NULL && g_utf8_strlen (issuer, -1) > 0) { g_string_append (uri, "&issuer="); g_string_append (uri, json_string_value (json_object_get (db_obj, "issuer"))); } gchar *str_to_append = NULL; g_string_append (uri, "&digits="); str_to_append = g_strdup_printf ("%lld", json_integer_value ( json_object_get (db_obj, "digits"))); g_string_append (uri,str_to_append); g_free (str_to_append); g_string_append (uri, "&algorithm="); g_string_append (uri, json_string_value ( json_object_get (db_obj, "algo"))); if (g_ascii_strcasecmp (json_string_value (json_object_get (db_obj, "type")), "TOTP") == 0) { g_string_append (uri, "&period="); str_to_append = g_strdup_printf ("%lld", json_integer_value ( json_object_get (db_obj, "period"))); g_string_append (uri, str_to_append); g_free (str_to_append); } else { g_string_append (uri, "&counter="); str_to_append = g_strdup_printf ("%lld", json_integer_value ( json_object_get (db_obj, "counter"))); g_string_append (uri, str_to_append); g_free (str_to_append); } g_string_append (uri, "\n"); g_free (constructed_label); g_free (escaped_label); return g_string_free (uri, FALSE); } static void parse_uri (const gchar *uri, GSList **otps) { const gchar *uri_copy = uri; if (g_ascii_strncasecmp (uri_copy, "otpauth://", 10) != 0) { return; } uri_copy += 10; otp_t *otp = g_new0 (otp_t, 1); // set default digits value to 6. If something else is specified, it will be read later on otp->digits = 6; if (g_ascii_strncasecmp (uri_copy, "totp/", 5) == 0) { otp->type = g_strdup ("TOTP"); otp->period = 30; } else if (g_ascii_strncasecmp (uri_copy, "hotp/", 5) == 0) { otp->type = g_strdup ("HOTP"); } else { g_free (otp); return; } uri_copy += 5; if (g_strrstr (uri_copy, "algorithm") == NULL) { // if the uri doesn't contain the algo parameter, fallback to sha1 otp->algo = g_strdup ("SHA1"); } parse_parameters (uri_copy, otp); *otps = g_slist_append (*otps, g_memdupX (otp, sizeof (otp_t))); g_free (otp); } static void parse_parameters (const gchar *modified_uri, otp_t *otp) { gchar **tokens = g_strsplit (modified_uri, "?", -1); gchar *escaped_issuer_and_label = g_uri_unescape_string (tokens[0], NULL); gchar *mod_uri_copy_utf8 = g_utf8_offset_to_pointer (modified_uri, g_utf8_strlen (tokens[0], -1) + 1); g_strfreev (tokens); tokens = g_strsplit (escaped_issuer_and_label, ":", -1); if (tokens[0] && tokens[1]) { otp->issuer = g_strdup (g_strstrip (tokens[0])); otp->account_name = g_strdup (g_strstrip (tokens[1])); } else { otp->account_name = g_strdup (g_strstrip (tokens[0])); } g_free (escaped_issuer_and_label); g_strfreev (tokens); tokens = g_strsplit (mod_uri_copy_utf8, "&", -1); gint i = 0; while (tokens[i]) { if (g_ascii_strncasecmp (tokens[i], "secret=", 7) == 0) { tokens[i] += 7; otp->secret = secure_strdup (tokens[i]); tokens[i] -= 7; } else if (g_ascii_strncasecmp (tokens[i], "algorithm=", 10) == 0) { tokens[i] += 10; if (g_ascii_strcasecmp (tokens[i], "SHA1") == 0 || g_ascii_strcasecmp (tokens[i], "SHA256") == 0 || g_ascii_strcasecmp (tokens[i], "SHA512") == 0) { otp->algo = g_ascii_strup (tokens[i], -1); } tokens[i] -= 10; } else if (g_ascii_strncasecmp (tokens[i], "period=", 7) == 0) { tokens[i] += 7; otp->period = (guint8) g_ascii_strtoll (tokens[i], NULL, 10); tokens[i] -= 7; } else if (g_ascii_strncasecmp (tokens[i], "digits=", 7) == 0) { tokens[i] += 7; otp->digits = (guint8) g_ascii_strtoll (tokens[i], NULL, 10); tokens[i] -= 7; } else if (g_ascii_strncasecmp (tokens[i], "issuer=", 7) == 0) { tokens[i] += 7; if (!otp->issuer) { otp->issuer = g_strdup (g_strstrip (tokens[i])); } tokens[i] -= 7; } else if (g_ascii_strncasecmp (tokens[i], "counter=", 8) == 0) { tokens[i] += 8; otp->counter = (guint64)g_ascii_strtoll (tokens[i], NULL, 10); tokens[i] -= 8; } i++; } g_strfreev (tokens); } OTPClient-3.2.1/src/parse-uri.h000066400000000000000000000004341452112020400161550ustar00rootroot00000000000000#pragma once #include #include "data.h" G_BEGIN_DECLS void set_otps_from_uris (const gchar *otpauth_uris, GSList **otps); gchar *get_otpauth_uri (AppData *app_data, json_t *obj); G_END_DECLSOTPClient-3.2.1/src/password-cb.c000066400000000000000000000151621452112020400164710ustar00rootroot00000000000000#include #include #include "gui-common.h" #include "message-dialogs.h" #include "get-builder.h" #include "otpclient.h" #include "common/common.h" typedef struct entrywidgets_t { GtkWidget *entry_old; GtkWidget *entry1; GtkWidget *entry2; gboolean retry; gchar *pwd; gchar *cur_pwd; } EntryWidgets; static void check_pwd_cb (GtkWidget *entry, gpointer user_data); static void password_cb (GtkWidget *entry, gpointer *pwd); gchar * prompt_for_password (AppData *app_data, gchar *current_key, const gchar *action_name, gboolean is_export_pwd) { EntryWidgets *entry_widgets = g_new0 (EntryWidgets, 1); entry_widgets->retry = FALSE; GtkBuilder *builder = get_builder_from_partial_path (UI_PARTIAL_PATH); GtkWidget *dialog; gboolean pwd_must_be_checked = TRUE; gboolean file_exists = g_file_test (app_data->db_data->db_path, G_FILE_TEST_EXISTS); if ((file_exists == TRUE || action_name != NULL) && current_key == NULL && is_export_pwd == FALSE) { // decrypt dialog, just one field pwd_must_be_checked = FALSE; dialog = GTK_WIDGET(gtk_builder_get_object (builder, "decpwd_diag_id")); gchar *text = NULL, *markup = NULL; if (action_name == NULL) { markup = g_markup_printf_escaped ("%s %s", "Enter the decryption password for\n", app_data->db_data->db_path); } else { text = g_strdup ("Enter the decryption password"); } GtkLabel *label = GTK_LABEL(gtk_builder_get_object (builder, "decpwd_label_id")); if (markup != NULL) { gtk_label_set_markup (label, markup); g_free (markup); } else { gtk_label_set_text (label, text); g_free (text); } entry_widgets->entry1 = GTK_WIDGET(gtk_builder_get_object (builder,"decpwddiag_entry_id")); g_signal_connect (entry_widgets->entry1, "activate", G_CALLBACK (send_ok_cb), NULL); g_signal_connect (entry_widgets->entry1, "icon-press", G_CALLBACK (icon_press_cb), NULL); } else if ((file_exists == FALSE && current_key == NULL) || is_export_pwd == TRUE) { // new db dialog, 2 fields dialog = GTK_WIDGET(gtk_builder_get_object (builder, "newdb_pwd_diag_id")); entry_widgets->entry1 = GTK_WIDGET(gtk_builder_get_object (builder,"newdb_pwd_diag_entry1_id")); entry_widgets->entry2 = GTK_WIDGET(gtk_builder_get_object (builder,"newdb_pwd_diag_entry2_id")); g_signal_connect (entry_widgets->entry2, "activate", G_CALLBACK (send_ok_cb), NULL); g_signal_connect (entry_widgets->entry1, "icon-press", G_CALLBACK (icon_press_cb), NULL); g_signal_connect (entry_widgets->entry2, "icon-press", G_CALLBACK (icon_press_cb), NULL); } else { // change pwd dialog, 3 fields if (current_key == NULL) { show_message_dialog (app_data->main_window, "ERROR: current_key cannot be NULL", GTK_MESSAGE_ERROR); g_free (entry_widgets); g_object_unref (builder); return NULL; } dialog = GTK_WIDGET(gtk_builder_get_object (builder, "changepwd_diag_id")); entry_widgets->cur_pwd = secure_strdup (current_key); entry_widgets->entry_old = GTK_WIDGET(gtk_builder_get_object (builder,"changepwd_diag_currententry_id")); entry_widgets->entry1 = GTK_WIDGET(gtk_builder_get_object (builder,"changepwd_diag_newentry1_id")); entry_widgets->entry2 = GTK_WIDGET(gtk_builder_get_object (builder,"changepwd_diag_newentry2_id")); g_signal_connect (entry_widgets->entry2, "activate", G_CALLBACK (send_ok_cb), NULL); g_signal_connect (entry_widgets->entry1, "icon-press", G_CALLBACK (icon_press_cb), NULL); g_signal_connect (entry_widgets->entry2, "icon-press", G_CALLBACK (icon_press_cb), NULL); g_signal_connect (entry_widgets->entry_old, "icon-press", G_CALLBACK (icon_press_cb), NULL); } gtk_window_set_transient_for (GTK_WINDOW(dialog), GTK_WINDOW(app_data->main_window)); gtk_widget_show_all (dialog); gint ret; do { ret = gtk_dialog_run (GTK_DIALOG(dialog)); if (ret == GTK_RESPONSE_OK) { if ((file_exists == TRUE || action_name != NULL) && pwd_must_be_checked == FALSE) { password_cb (entry_widgets->entry1, (gpointer *)&entry_widgets->pwd); } else { check_pwd_cb (entry_widgets->entry1, (gpointer)entry_widgets); } } } while (ret == GTK_RESPONSE_OK && entry_widgets->retry == TRUE); gchar *pwd = NULL; if (entry_widgets->pwd != NULL) { gcry_free (current_key); gsize len = g_utf8_strlen (entry_widgets->pwd, -1) + 1; pwd = gcry_calloc_secure (len, 1); strncpy (pwd, entry_widgets->pwd, len); gcry_free (entry_widgets->pwd); } if (entry_widgets->cur_pwd != NULL) { gcry_free (entry_widgets->cur_pwd); } g_free (entry_widgets); gtk_widget_destroy (dialog); g_object_unref (builder); return pwd; } static void check_pwd_cb (GtkWidget *entry, gpointer user_data) { EntryWidgets *entry_widgets = (EntryWidgets *) user_data; if (entry_widgets->cur_pwd != NULL && g_strcmp0 (gtk_entry_get_text (GTK_ENTRY(entry_widgets->entry_old)), entry_widgets->cur_pwd) != 0) { show_message_dialog (gtk_widget_get_toplevel (entry), "Old password doesn't match", GTK_MESSAGE_ERROR); entry_widgets->retry = TRUE; return; } if (gtk_entry_get_text_length (GTK_ENTRY(entry_widgets->entry1)) < 6) { show_message_dialog (gtk_widget_get_toplevel (entry), "Password must be at least 6 characters.", GTK_MESSAGE_ERROR); entry_widgets->retry = TRUE; return; } if (g_strcmp0 (gtk_entry_get_text (GTK_ENTRY(entry_widgets->entry1)), gtk_entry_get_text (GTK_ENTRY(entry_widgets->entry2))) == 0) { password_cb (entry, (gpointer *)&entry_widgets->pwd); entry_widgets->retry = FALSE; } else { show_message_dialog (gtk_widget_get_toplevel (entry), "Passwords mismatch", GTK_MESSAGE_ERROR); entry_widgets->retry = TRUE; } } static void password_cb (GtkWidget *entry, gpointer *pwd) { const gchar *text = gtk_entry_get_text (GTK_ENTRY(entry)); gsize len = g_utf8_strlen (text, -1) + 1; *pwd = gcry_calloc_secure (len, 1); strncpy (*pwd, text, len); GtkWidget *top_level = gtk_widget_get_toplevel (entry); gtk_dialog_response (GTK_DIALOG (top_level), GTK_RESPONSE_CLOSE); } OTPClient-3.2.1/src/password-cb.h000066400000000000000000000003101452112020400164630ustar00rootroot00000000000000#pragma once #include #include "data.h" G_BEGIN_DECLS gchar *prompt_for_password (AppData *app_data, gchar *current_key, const gchar *action_name, gboolean is_export_pwd); G_END_DECLS OTPClient-3.2.1/src/qrcode-parser.c000066400000000000000000000077231452112020400170200ustar00rootroot00000000000000#include #include #include #include #include #include "common/common.h" typedef struct image_data_t { gulong width; gulong height; guchar *raw_data; } ImageData; static gchar *set_data_from_png (const gchar *png_path, ImageData *image_data); gchar * parse_qrcode (const gchar *png_path, gchar **otpauth_uri) { zbar_image_scanner_t *scanner = zbar_image_scanner_create (); zbar_image_scanner_set_config (scanner, ZBAR_NONE, ZBAR_CFG_ENABLE, 1); ImageData *image_data = g_new0 (ImageData, 1); gchar *err_msg = set_data_from_png (png_path, image_data); if (err_msg != NULL) { g_free (image_data); zbar_image_scanner_destroy (scanner); return err_msg; } zbar_image_t *image = zbar_image_create (); zbar_image_set_format (image, zbar_fourcc ('Y','8','0','0')); zbar_image_set_size (image, image_data->width, image_data->height); zbar_image_set_data (image, image_data->raw_data, image_data->width * image_data->height, zbar_image_free_data); gint n = zbar_scan_image (scanner, image); if (n < 1) { zbar_image_destroy (image); zbar_image_scanner_destroy (scanner); g_free (image_data); return g_strdup ("Couldn't find a valid qrcode"); } const zbar_symbol_t *symbol = zbar_image_first_symbol (image); for (; symbol; symbol = zbar_symbol_next (symbol)) { gchar *unesc_str = g_uri_unescape_string_secure (zbar_symbol_get_data (symbol), NULL); *otpauth_uri = secure_strdup (unesc_str); gcry_free (unesc_str); } zbar_image_destroy (image); zbar_image_scanner_destroy (scanner); g_free (image_data); return NULL; } static gchar * set_data_from_png (const gchar *png_path, ImageData *image_data) { FILE *file = g_fopen (png_path, "rb"); if (file == NULL) { return g_strdup ("Couldn't open the PNG file"); } guchar sig[8]; if (fread (sig, 1, 8, file) != 8) { fclose (file); return g_strdup ("Couldn't read signature from PNG file"); } if (!png_check_sig (sig, 8)) { fclose (file); return g_strdup ("The file is not a PNG image"); } png_structp png = png_create_read_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (png == NULL) { fclose (file); return g_strdup ("png_create_read_struct failed"); } png_infop info = png_create_info_struct (png); if (!info) { png_destroy_read_struct (&png, NULL, NULL); fclose (file); return g_strdup ("png_create_info_struct failed"); } if (setjmp (png_jmpbuf (png))) { png_destroy_read_struct (&png, &info, NULL); fclose (file); return g_strdup ("setjmp failed"); } png_init_io (png, file); png_set_sig_bytes (png, 8); png_read_info (png, info); gint color = png_get_color_type (png, info); gint bits = png_get_bit_depth (png, info); if (color & PNG_COLOR_TYPE_PALETTE) { png_set_palette_to_rgb (png); } if (color == PNG_COLOR_TYPE_GRAY && bits < 8) { png_set_expand_gray_1_2_4_to_8 (png); } if (bits == 16) { png_set_strip_16 (png); } if (color & PNG_COLOR_MASK_ALPHA) { png_set_strip_alpha (png); } if (color & PNG_COLOR_MASK_COLOR) { png_set_rgb_to_gray_fixed (png, 1, -1, -1); } image_data->width = (guint)png_get_image_width (png, info); image_data->height = (guint)png_get_image_height (png, info); image_data->raw_data = (guchar *)g_malloc0 (image_data->width * image_data->height); png_bytep rows[image_data->height]; for (gint i = 0; i < image_data->height; i++) { rows[i] = image_data->raw_data + (image_data->width * i); } png_read_image (png, rows); png_read_end (png, NULL); png_destroy_read_struct (&png, &info, NULL); fclose (file); return NULL; } OTPClient-3.2.1/src/qrcode-parser.h000066400000000000000000000002351452112020400170140ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS gchar *parse_qrcode (const gchar *png_path, gchar **otpauth_uri); G_END_DECLS OTPClient-3.2.1/src/secret-schema.c000066400000000000000000000030301452112020400167570ustar00rootroot00000000000000#include const SecretSchema * otpclient_get_schema (void) { static const SecretSchema the_schema = { "com.github.paolostivanin.OTPClient", SECRET_SCHEMA_NONE, { { "string", SECRET_SCHEMA_ATTRIBUTE_STRING }, { "NULL", 0 }, }, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, }; return &the_schema; } void on_password_stored (GObject *source __attribute__((unused)), GAsyncResult *result, gpointer unused __attribute__((unused))) { GError *error = NULL; secret_password_store_finish (result, &error); if (error != NULL) { g_printerr ("Couldn't store the password in the secret service.\n" "The error was: %s", error->message); g_error_free (error); } } void on_password_cleared (GObject *source __attribute__((unused)), GAsyncResult *result, gpointer unused __attribute__((unused))) { GError *error = NULL; gboolean removed = secret_password_clear_finish (result, &error); if (error != NULL) { g_printerr ("Couldn't remove the password in the secret service.\n" "The error was: %s", error->message); g_error_free (error); } if (removed == TRUE) { g_print ("Password successfully removed from the secret service.\n"); } }OTPClient-3.2.1/src/secret-schema.h000066400000000000000000000007011452112020400167660ustar00rootroot00000000000000#pragma once #include const SecretSchema *otpclient_get_schema (void) G_GNUC_CONST; #define OTPCLIENT_SCHEMA otpclient_get_schema () void on_password_stored (GObject *source, GAsyncResult *result, gpointer unused); void on_password_cleared (GObject *source, GAsyncResult *result, gpointer unused);OTPClient-3.2.1/src/settings-cb.c000066400000000000000000000311301452112020400164600ustar00rootroot00000000000000#include #include #include #include "otpclient.h" #include "message-dialogs.h" #include "get-builder.h" #include "secret-schema.h" #include "common/common.h" typedef struct settings_data_t { GtkWidget *dss_switch; GtkWidget *al_switch; GtkWidget *inactivity_cb; AppData *app_data; } SettingsData; static void handle_al_ss (AppData *app_data, GtkWidget *al_switch, GtkWidget *inactivity_cb, GtkWidget *dss_switch); static gboolean handle_secretservice_switch (GtkSwitch *sw, gboolean state, gpointer user_data); static void handle_secretservice_combobox (GtkComboBox *cb, gpointer user_data); static gboolean handle_autolock (GtkSwitch *sw, gboolean state, gpointer user_data); void settings_dialog_cb (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; SettingsData *settings_data = g_new0 (SettingsData, 1); settings_data->app_data = app_data; gchar *cfg_file_path; #ifndef USE_FLATPAK_APP_FOLDER cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); #else cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); #endif GError *err = NULL; GKeyFile *kf = g_key_file_new (); if (!g_key_file_load_from_file (kf, cfg_file_path, G_KEY_FILE_NONE, &err)) { gchar *msg = g_strconcat ("Couldn't get data from config file: ", err->message, NULL); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); g_free (cfg_file_path); g_key_file_free (kf); g_clear_error (&err); g_free (settings_data); return; } // if key is not found, g_key_file_get_boolean returns FALSE and g_key_file_get_integer returns 0. // Therefore, having these values as default is exactly what we want. So no need to check whether the key is missing. app_data->show_next_otp = g_key_file_get_boolean (kf, "config", "show_next_otp", NULL); app_data->disable_notifications = g_key_file_get_boolean (kf, "config", "notifications", NULL); app_data->search_column = g_key_file_get_integer (kf, "config", "search_column", NULL); app_data->auto_lock = g_key_file_get_boolean (kf, "config", "auto_lock", NULL); app_data->inactivity_timeout = g_key_file_get_integer (kf, "config", "inactivity_timeout", NULL); app_data->use_dark_theme = g_key_file_get_boolean (kf, "config", "dark_theme", NULL); app_data->use_secret_service = g_key_file_get_boolean (kf, "config", "use_secret_service", &err); if (err != NULL && g_error_matches (err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { // if the key is not found, we set it to TRUE and save it to the config file. app_data->use_secret_service = TRUE; g_clear_error (&err); } GtkBuilder *builder = get_builder_from_partial_path(UI_PARTIAL_PATH); GtkWidget *dialog = GTK_WIDGET(gtk_builder_get_object (builder, "settings_diag_id")); GtkWidget *sno_switch = GTK_WIDGET(gtk_builder_get_object (builder, "nextotp_switch_id")); GtkWidget *dn_switch = GTK_WIDGET(gtk_builder_get_object (builder, "notif_switch_id")); GtkWidget *sc_cb = GTK_WIDGET(gtk_builder_get_object (builder, "search_by_cb_id")); settings_data->al_switch = GTK_WIDGET(gtk_builder_get_object (builder, "autolock_switch_id")); g_signal_connect (settings_data->al_switch, "state-set", G_CALLBACK(handle_secretservice_switch), settings_data); settings_data->inactivity_cb = GTK_WIDGET(gtk_builder_get_object (builder, "autolock_inactive_cb_id")); g_signal_connect (settings_data->inactivity_cb, "changed", G_CALLBACK(handle_secretservice_combobox), settings_data); GtkWidget *dt_switch = GTK_WIDGET(gtk_builder_get_object (builder, "dark_theme_switch_id")); settings_data->dss_switch = GTK_WIDGET(gtk_builder_get_object (builder, "secret_service_switch_id")); g_signal_connect (settings_data->dss_switch, "state-set", G_CALLBACK(handle_autolock), settings_data); gtk_window_set_transient_for (GTK_WINDOW(dialog), GTK_WINDOW(app_data->main_window)); gtk_switch_set_active (GTK_SWITCH(sno_switch), app_data->show_next_otp); gtk_switch_set_active (GTK_SWITCH(dn_switch), app_data->disable_notifications); gtk_switch_set_active (GTK_SWITCH(settings_data->al_switch), app_data->auto_lock); gtk_switch_set_active (GTK_SWITCH(dt_switch), app_data->use_dark_theme); gtk_switch_set_active (GTK_SWITCH(settings_data->dss_switch), app_data->use_secret_service); gchar *active_id_string = g_strdup_printf ("%d", app_data->search_column); gtk_combo_box_set_active_id (GTK_COMBO_BOX(sc_cb), active_id_string); g_free (active_id_string); active_id_string = g_strdup_printf ("%d", app_data->inactivity_timeout); gtk_combo_box_set_active_id (GTK_COMBO_BOX(settings_data->inactivity_cb), active_id_string); g_free (active_id_string); handle_al_ss (app_data, settings_data->al_switch, settings_data->inactivity_cb, settings_data->dss_switch); gtk_widget_show_all (dialog); gboolean old_ss_value = app_data->use_secret_service; switch (gtk_dialog_run (GTK_DIALOG(dialog))) { case GTK_RESPONSE_OK: app_data->show_next_otp = gtk_switch_get_active (GTK_SWITCH(sno_switch)); app_data->disable_notifications = gtk_switch_get_active (GTK_SWITCH(dn_switch)); app_data->search_column = (gint)g_ascii_strtoll (gtk_combo_box_get_active_id (GTK_COMBO_BOX(sc_cb)), NULL, 10); app_data->auto_lock = gtk_switch_get_active (GTK_SWITCH(settings_data->al_switch)); app_data->inactivity_timeout = (gint)g_ascii_strtoll (gtk_combo_box_get_active_id (GTK_COMBO_BOX(settings_data->inactivity_cb)), NULL, 10); app_data->use_dark_theme = gtk_switch_get_active (GTK_SWITCH(dt_switch)); app_data->use_secret_service = gtk_switch_get_active (GTK_SWITCH(settings_data->dss_switch)); g_key_file_set_boolean (kf, "config", "show_next_otp", app_data->show_next_otp); g_key_file_set_boolean (kf, "config", "notifications", app_data->disable_notifications); g_key_file_set_integer (kf, "config", "search_column", app_data->search_column); g_key_file_set_boolean (kf, "config", "auto_lock", app_data->auto_lock); g_key_file_set_integer (kf, "config", "inactivity_timeout", app_data->inactivity_timeout); g_key_file_set_boolean (kf, "config", "dark_theme", app_data->use_dark_theme); g_key_file_set_boolean (kf, "config", "use_secret_service", app_data->use_secret_service); if (old_ss_value == TRUE && app_data->use_secret_service == FALSE) { // secret service was just disabled, so we have to clear the password from the keyring secret_password_clear (OTPCLIENT_SCHEMA, NULL, on_password_cleared, NULL, "string", "main_pwd", NULL); } if (!g_key_file_save_to_file (kf, cfg_file_path, NULL)) { g_printerr ("%s\n", _("Error while saving the config file.")); } gtk_tree_view_set_search_column (GTK_TREE_VIEW(app_data->tree_view), app_data->search_column + 1); break; case GTK_RESPONSE_CANCEL: break; } g_free (cfg_file_path); g_key_file_free (kf); g_free (settings_data); gtk_widget_destroy (dialog); g_object_unref (builder); } void show_settings_cb_shortcut (GtkWidget *w __attribute__((unused)), gpointer user_data) { settings_dialog_cb (NULL, NULL, user_data); } static void handle_al_ss (AppData *app_data, GtkWidget *al_switch, GtkWidget *inactivity_cb, GtkWidget *dss_switch) { GKeyFile *kf = get_kf_ptr (); if (app_data->use_secret_service == TRUE) { // secret service is enabled, so we need to disable auto-lock app_data->auto_lock = FALSE; app_data->inactivity_timeout = 0; gtk_widget_set_sensitive (al_switch, FALSE); gtk_widget_set_sensitive (inactivity_cb, FALSE); } else { if (app_data->auto_lock == TRUE || app_data->inactivity_timeout > 0) { // if secret service is disabled AND (auto-lock is enabled OR timeout is enabled), we need to disable secret service app_data->use_secret_service = FALSE; gtk_widget_set_sensitive (dss_switch, FALSE); } } if (kf != NULL) { // Until the migration is done for all users, we need to manually update the settings. // This code block can be removed once all distros have upgrade to, at least, version 3.1.4. g_key_file_set_boolean (kf, "config", "auto_lock", app_data->auto_lock); g_key_file_set_boolean (kf, "config", "use_secret_service", app_data->use_secret_service); g_key_file_set_integer (kf, "config", "inactivity_timeout", app_data->inactivity_timeout); gchar *cfg_file_path; #ifndef USE_FLATPAK_APP_FOLDER cfg_file_path = g_build_filename (g_get_user_config_dir (), "otpclient.cfg", NULL); #else cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); #endif if (!g_key_file_save_to_file (kf, cfg_file_path, NULL)) { g_printerr ("%s\n", _("Error while saving the config file.")); } g_key_file_free (kf); } } static gboolean handle_secretservice_switch (GtkSwitch *sw, gboolean state, gpointer user_data) { /* SecretService is disabled (TRUE), and we disable both autolock (FALSE) AND autolock timeout (0): * - secret_service_switch_id must be set to sensitive */ SettingsData *settings_data = (SettingsData *)user_data; settings_data->app_data->auto_lock = state; if (state == FALSE && settings_data->app_data->inactivity_timeout == 0) { gtk_widget_set_sensitive (settings_data->dss_switch, TRUE); } else { gtk_widget_set_sensitive (settings_data->dss_switch, FALSE); } gtk_switch_set_state (GTK_SWITCH(sw), state); return TRUE; } static void handle_secretservice_combobox (GtkComboBox *cb, gpointer user_data) { /* SecretService is disabled (TRUE), and we disable both autolock (FALSE) AND autolock timeout (0): * - secret_service_switch_id must be set to sensitive */ SettingsData *settings_data = (SettingsData *)user_data; settings_data->app_data->inactivity_timeout = (gint)g_ascii_strtoll (gtk_combo_box_get_active_id (GTK_COMBO_BOX(cb)), NULL, 10); if (settings_data->app_data->inactivity_timeout == 0 && settings_data->app_data->auto_lock == FALSE) { gtk_widget_set_sensitive (settings_data->dss_switch, TRUE); } else { gtk_widget_set_sensitive (settings_data->dss_switch, FALSE); } } static gboolean handle_autolock (GtkSwitch *sw __attribute__((unused)), gboolean state, gpointer user_data) { /* SecretService is enabled, and we disable it (TRUE -> FALSE): * - lock_btn_id, autolock_switch_id and autolock_inactive_cb_id must be set to sensitive * - add entry signal ctrl-l */ SettingsData *settings_data = (SettingsData *)user_data; if (state == FALSE) { gtk_widget_set_sensitive (GTK_WIDGET(gtk_builder_get_object (settings_data->app_data->builder, "lock_btn_id")), TRUE); gtk_widget_set_sensitive (settings_data->al_switch, TRUE); gtk_widget_set_sensitive (settings_data->inactivity_cb, TRUE); gtk_binding_entry_add_signal (gtk_binding_set_by_class (GTK_APPLICATION_WINDOW_GET_CLASS(settings_data->app_data->main_window)), GDK_KEY_l, GDK_CONTROL_MASK, "lock-app", 0); } else { gtk_widget_set_sensitive (GTK_WIDGET(gtk_builder_get_object (settings_data->app_data->builder, "lock_btn_id")), FALSE); gtk_widget_set_sensitive (settings_data->al_switch, FALSE); gtk_widget_set_sensitive (settings_data->inactivity_cb, FALSE); gtk_binding_entry_remove (gtk_binding_set_by_class (GTK_APPLICATION_WINDOW_GET_CLASS(settings_data->app_data->main_window)), GDK_KEY_l, GDK_CONTROL_MASK); } gtk_switch_set_state (GTK_SWITCH(sw), state); return TRUE; }OTPClient-3.2.1/src/settings-cb.h000066400000000000000000000005361452112020400164730ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS void settings_dialog_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void show_settings_cb_shortcut (GtkWidget *w, gpointer user_data); G_END_DECLSOTPClient-3.2.1/src/setup-signals-shortcuts.c000066400000000000000000000104531452112020400210750ustar00rootroot00000000000000#include #include "data.h" #include "change-pwd-cb.h" #include "settings-cb.h" #include "shortcuts-cb.h" #include "webcam-add-cb.h" #include "manual-add-cb.h" #include "edit-row-cb.h" #include "show-qr-cb.h" #include "change-db-cb.h" static void setup_signals (void); static void connect_signals (AppData *app_data); void setup_kb_shortcuts (AppData *app_data) { // Used letters: r,d,l,h,w,m,b,o,e,s,k // hide-all-otps is in src/treeview.c setup_signals (); connect_signals (app_data); GtkBindingSet *mw_binding_set = gtk_binding_set_by_class (GTK_APPLICATION_WINDOW_GET_CLASS(app_data->main_window)); gtk_binding_entry_add_signal(mw_binding_set, GDK_KEY_r, GDK_CONTROL_MASK, "toggle-reorder-button", 0); gtk_binding_entry_add_signal (mw_binding_set, GDK_KEY_d, GDK_CONTROL_MASK, "toggle-delete-button", 0); if (app_data->auto_lock == TRUE || app_data->inactivity_timeout > 0) { // auto-lock is enabled, so secret service is disabled, therefore we allow the shortcut gtk_binding_entry_add_signal (mw_binding_set, GDK_KEY_l, GDK_CONTROL_MASK, "lock-app", 0); } gtk_binding_entry_add_signal (mw_binding_set, GDK_KEY_b, GDK_CONTROL_MASK, "change-db", 0); gtk_binding_entry_add_signal (mw_binding_set, GDK_KEY_o, GDK_CONTROL_MASK, "change-pwd", 0); gtk_binding_entry_add_signal (mw_binding_set, GDK_KEY_s, GDK_CONTROL_MASK, "show-settings", 0); gtk_binding_entry_add_signal (mw_binding_set, GDK_KEY_k, GDK_CONTROL_MASK, "show-kb-shortcuts", 0); // GDM_MOD1_MASK: the fourth modifier key (it depends on the modifier mapping of the X server which key is interpreted as this modifier, but normally it is the Alt key). gtk_binding_entry_add_signal (mw_binding_set, GDK_KEY_w, GDK_MOD1_MASK, "scan-webcam", 0); gtk_binding_entry_add_signal (mw_binding_set, GDK_KEY_m, GDK_MOD1_MASK, "manual-add", 0); gtk_binding_entry_add_signal (mw_binding_set, GDK_KEY_e, GDK_MOD1_MASK, "edit-row", 0); gtk_binding_entry_add_signal (mw_binding_set, GDK_KEY_q, GDK_MOD1_MASK, "show-qr", 0); } static void setup_signals (void) { g_signal_new ("toggle-reorder-button", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new ("toggle-delete-button", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new ("lock-app", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new ("change-db", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new ("change-pwd", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new ("show-settings", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new ("show-kb-shortcuts", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new ("scan-webcam", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new ("manual-add", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new ("edit-row", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_signal_new ("show-qr", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } static void connect_signals (AppData *app_data) { g_signal_connect (app_data->main_window, "change-db", G_CALLBACK(change_db_cb_shortcut), app_data); g_signal_connect (app_data->main_window, "change-pwd", G_CALLBACK(change_pwd_cb_shortcut), app_data); g_signal_connect (app_data->main_window, "show-settings", G_CALLBACK(show_settings_cb_shortcut), app_data); g_signal_connect (app_data->main_window, "show-kb-shortcuts", G_CALLBACK(show_kbs_cb_shortcut), app_data); g_signal_connect (app_data->main_window, "scan-webcam", G_CALLBACK(webcam_add_cb_shortcut), app_data); g_signal_connect (app_data->main_window, "manual-add", G_CALLBACK(manual_add_cb_shortcut), app_data); g_signal_connect (app_data->main_window, "edit-row", G_CALLBACK(edit_row_cb_shortcut), app_data); g_signal_connect (app_data->main_window, "show-qr", G_CALLBACK(show_qr_cb_shortcut), app_data); }OTPClient-3.2.1/src/shortcuts-cb.c000066400000000000000000000015071452112020400166630ustar00rootroot00000000000000#include #include "message-dialogs.h" #include "get-builder.h" #include "data.h" void shortcuts_window_cb (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; GtkBuilder *builder = get_builder_from_partial_path ("share/otpclient/shortcuts.ui"); GtkWidget *overlay = GTK_WIDGET (gtk_builder_get_object (builder, "shortcuts-otpclient")); gtk_window_set_transient_for (GTK_WINDOW(overlay), GTK_WINDOW(app_data->main_window)); gtk_widget_show (overlay); g_object_unref (builder); } void show_kbs_cb_shortcut (GtkWidget *w __attribute__((unused)), gpointer user_data) { shortcuts_window_cb (NULL, NULL, user_data); }OTPClient-3.2.1/src/shortcuts-cb.h000066400000000000000000000005071452112020400166670ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS void shortcuts_window_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void show_kbs_cb_shortcut (GtkWidget *w, gpointer user_data); G_END_DECLS OTPClient-3.2.1/src/show-qr-cb.c000066400000000000000000000124371452112020400162310ustar00rootroot00000000000000#include #include #include #include #include #include "data.h" #include "parse-uri.h" #include "get-builder.h" #include "message-dialogs.h" #define INCHES_PER_METER (100.0/2.54) #define SIZE 3 #define MARGIN 2 #define DPI 72 #define PNG_OUT "/tmp/qrcode_otpclient.png" static int write_png (const QRcode *qrcode); void show_qr_cb (GSimpleAction *simple __attribute__((unused)), GVariant *parameter __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; gchar *otpauth_uri = get_otpauth_uri (app_data, NULL); if (otpauth_uri == NULL) { show_message_dialog (app_data->main_window, "Error: a row must be selected in order to get the QR Code.", GTK_MESSAGE_ERROR); return; } QRcode *qr = QRcode_encodeString8bit ((const gchar *)otpauth_uri, 0, QR_ECLEVEL_H); write_png (qr); g_free (otpauth_uri); GtkBuilder *builder = get_builder_from_partial_path (UI_PARTIAL_PATH); GtkWidget *image = GTK_WIDGET(gtk_builder_get_object (builder, "qr_code_gtkimage_id")); GtkWidget *diag = GTK_WIDGET(gtk_builder_get_object (builder, "qr_code_diag_id")); GError *err = NULL; GdkPixbuf *pbuf = gdk_pixbuf_new_from_file (PNG_OUT, &err); if (err != NULL) { g_printerr ("Couldn't load the image: %s\n", err->message); return; } gtk_image_set_from_pixbuf (GTK_IMAGE(image), pbuf); gtk_widget_show_all (diag); gint response = gtk_dialog_run (GTK_DIALOG(diag)); if (response == GTK_RESPONSE_OK) { gtk_widget_destroy (diag); g_object_unref (pbuf); g_object_unref (builder); if (g_unlink (PNG_OUT) == -1) { g_printerr ("%s\n", _("Couldn't unlink the PNG file.")); } } } void show_qr_cb_shortcut (GtkWidget *w __attribute__((unused)), gpointer user_data) { show_qr_cb (NULL, NULL, user_data); } static int write_png (const QRcode *qrcode) { guint realwidth = (qrcode->width + MARGIN * 2) * SIZE; guchar *row = (guchar *)g_malloc0 ((size_t)((realwidth + 7) / 8)); if (row == NULL) { g_printerr ("Failed to allocate memory.\n"); return -1; } png_structp png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (png_ptr == NULL) { g_printerr ("Failed to initialize PNG writer.\n"); g_free (row); return -1; } png_infop info_ptr = png_create_info_struct (png_ptr); if (info_ptr == NULL) { g_printerr ("Failed to initialize PNG write.\n"); g_free (row); return -1; } if (setjmp (png_jmpbuf(png_ptr))) { png_destroy_write_struct (&png_ptr, &info_ptr); g_printerr ("Failed to write PNG image.\n"); g_free (row); return -1; } png_colorp palette = (png_colorp)g_malloc0 (sizeof (png_color) * 2); if (palette == NULL) { g_printerr ("Failed to allocate memory.\n"); g_free (row); return -1; } guchar fg_color[4] = {0, 0, 0, 255}; guchar bg_color[4] = {255, 255, 255, 255}; png_byte alpha_values[2]; palette[0].red = fg_color[0]; palette[0].green = fg_color[1]; palette[0].blue = fg_color[2]; palette[1].red = bg_color[0]; palette[1].green = bg_color[1]; palette[1].blue = bg_color[2]; alpha_values[0] = fg_color[3]; alpha_values[1] = bg_color[3]; png_set_PLTE(png_ptr, info_ptr, palette, 2); png_set_tRNS(png_ptr, info_ptr, alpha_values, 2, NULL); FILE *fp = fopen (PNG_OUT, "wb"); if (fp == NULL) { g_printerr ("Failed to create file: %s\n", PNG_OUT); g_free (row); return -1; } png_init_io (png_ptr, fp); png_set_IHDR (png_ptr, info_ptr, (guint)realwidth, (guint)realwidth, 1, PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); png_set_pHYs (png_ptr, info_ptr, DPI * INCHES_PER_METER, DPI * INCHES_PER_METER, PNG_RESOLUTION_METER); png_write_info (png_ptr, info_ptr); memset (row, 0xff, (size_t)((realwidth + 7) / 8)); for (gint y = 0; y < MARGIN * SIZE; y++) { png_write_row (png_ptr, row); } gint bit; guchar *q; guchar *p = qrcode->data; for (gint y = 0; y < qrcode->width; y++) { memset (row, 0xff, (size_t)((realwidth + 7) / 8)); q = row + MARGIN * SIZE / 8; bit = 7 - (MARGIN * SIZE % 8); for (gint x = 0; x < qrcode->width; x++) { for (gint xx = 0; xx < SIZE; xx++) { *q ^= (*p & 1) << bit; bit--; if (bit < 0) { q++; bit = 7; } } p++; } for (gint yy = 0; yy < SIZE; yy++) { png_write_row (png_ptr, row); } } memset (row, 0xff, (size_t)((realwidth + 7) / 8)); for (gint y = 0; y < MARGIN * SIZE; y++) { png_write_row (png_ptr, row); } png_write_end (png_ptr, info_ptr); png_destroy_write_struct (&png_ptr, &info_ptr); fclose (fp); g_free (row); g_free (palette); return 0; }OTPClient-3.2.1/src/show-qr-cb.h000066400000000000000000000005001452112020400162220ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS void show_qr_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void show_qr_cb_shortcut (GtkWidget *w, gpointer user_data); G_END_DECLS OTPClient-3.2.1/src/treeview.c000066400000000000000000000352521452112020400161010ustar00rootroot00000000000000#include #include #include "otpclient.h" #include "liststore-misc.h" #include "message-dialogs.h" #include "common/common.h" typedef struct parsed_json_data_t { gchar **types; gchar **labels; gchar **issuers; GArray *periods; } ParsedData; static void set_json_data (json_t *array, ParsedData *pjd); static void add_data_to_model (DatabaseData *db_data, GtkListStore *store); static void add_columns (GtkTreeView *tree_view); static void hide_all_otps_cb (GtkTreeView *tree_view, gpointer user_data); static gboolean clear_all_otps (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data); static void free_pjd (ParsedData *pjd); void create_treeview (AppData *app_data) { app_data->tree_view = GTK_TREE_VIEW(gtk_builder_get_object (app_data->builder, "treeview_id")); GtkListStore *list_store = gtk_list_store_new (NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_INT); add_columns (app_data->tree_view); add_data_to_model (app_data->db_data, list_store); gtk_tree_view_set_model (app_data->tree_view, GTK_TREE_MODEL(list_store)); // model has id 0 for type, 1 for label, 2 for issuer, etc while ui file has 0 label and 1 issuer. That's why the "+1" gtk_tree_view_set_search_column (GTK_TREE_VIEW(app_data->tree_view), app_data->search_column + 1); GtkBindingSet *tv_binding_set = gtk_binding_set_by_class (GTK_TREE_VIEW_GET_CLASS(app_data->tree_view)); g_signal_new ("hide-all-otps", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); gtk_binding_entry_add_signal (tv_binding_set, GDK_KEY_h, GDK_MOD1_MASK, "hide-all-otps", 0); // signal emitted when row is selected g_signal_connect (app_data->tree_view, "row-activated", G_CALLBACK(row_selected_cb), app_data); // signal emitted when CTRL+H is pressed g_signal_connect (app_data->tree_view, "hide-all-otps", G_CALLBACK(hide_all_otps_cb), app_data); g_object_unref (list_store); } void update_model (AppData *app_data) { if (app_data->tree_view != NULL) { GtkListStore *store = GTK_LIST_STORE(gtk_tree_view_get_model (app_data->tree_view)); gtk_list_store_clear (store); add_data_to_model (app_data->db_data, store); } } void delete_rows_cb (GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; g_return_if_fail (tree_view != NULL); GtkTreeModel *model = gtk_tree_view_get_model (tree_view); GtkListStore *list_store = GTK_LIST_STORE(model); GtkTreeIter iter; gtk_tree_model_get_iter (model, &iter, path); gboolean delete_entry = FALSE; GtkWidget *del_diag = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "del_diag_id")); gtk_window_set_transient_for (GTK_WINDOW(del_diag), GTK_WINDOW(app_data->main_window)); gint res = gtk_dialog_run (GTK_DIALOG(del_diag)); switch (res) { case GTK_RESPONSE_YES: delete_entry = TRUE; break; case GTK_RESPONSE_NO: default: delete_entry = FALSE; break; } gtk_widget_hide (del_diag); if (delete_entry == FALSE) { return; } gint db_item_position_to_delete; gtk_tree_model_get (model, &iter, COLUMN_POSITION_IN_DB, &db_item_position_to_delete, -1); json_array_remove (app_data->db_data->json_data, db_item_position_to_delete); gtk_list_store_remove (list_store, &iter); // json_array_remove shifts all items, so we have to take care of updating the real item's position in the database gint row_db_pos; gboolean valid = gtk_tree_model_get_iter_first(model, &iter); while (valid) { gtk_tree_model_get (model, &iter, COLUMN_POSITION_IN_DB, &row_db_pos, -1); if (row_db_pos > db_item_position_to_delete) { gint shifted_position = row_db_pos - 1; gtk_list_store_set (list_store, &iter, COLUMN_POSITION_IN_DB, shifted_position, -1); } valid = gtk_tree_model_iter_next(model, &iter); } GError *err = NULL; update_and_reload_db (app_data, app_data->db_data, FALSE, &err); if (err != NULL) { gchar *msg = g_strconcat ("The database update FAILED. The error message is:\n", err->message, NULL); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); } } void row_selected_cb (GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column __attribute__((unused)), gpointer user_data) { AppData *app_data = (AppData *)user_data; if (app_data->is_reorder_active == FALSE) { GtkTreeModel *model = gtk_tree_view_get_model (tree_view); GtkTreeIter iter; gtk_tree_model_get_iter (model, &iter, path); gchar *otp_type, *otp_value; gtk_tree_model_get (model, &iter, COLUMN_TYPE, &otp_type, -1); gtk_tree_model_get (model, &iter, COLUMN_OTP, &otp_value, -1); GDateTime *now = g_date_time_new_now_local (); GTimeSpan diff = g_date_time_difference (now, app_data->db_data->last_hotp_update); if (otp_value != NULL && g_utf8_strlen (otp_value, -1) > 3) { // OTP is already set, so we update the value only if it is an HOTP if (g_ascii_strcasecmp (otp_type, "HOTP") == 0) { if (diff >= G_USEC_PER_SEC * HOTP_RATE_LIMIT_IN_SEC) { set_otp (GTK_LIST_STORE (model), iter, app_data); g_free (otp_value); gtk_tree_model_get (model, &iter, COLUMN_OTP, &otp_value, -1); } } } else { // OTP is not already set, so we set it set_otp (GTK_LIST_STORE (model), iter, app_data); g_free (otp_value); gtk_tree_model_get (model, &iter, COLUMN_OTP, &otp_value, -1); } // and, in any case, we copy the otp to the clipboard and send a notification gtk_clipboard_set_text (app_data->clipboard, otp_value, -1); if (!app_data->disable_notifications) { g_application_send_notification (G_APPLICATION(gtk_window_get_application (GTK_WINDOW (app_data->main_window))), NOTIFICATION_ID, app_data->notification); } g_date_time_unref (now); g_free (otp_type); g_free (otp_value); } } void reorder_db (AppData *app_data) { // Iter through all rows. If the position in treeview is different from current_db_pos, then compute hash and add (hash,newpos) to the list GSList *nodes_order_slist = NULL; GtkTreeIter iter; guint current_db_pos; GtkTreeModel *model = gtk_tree_view_get_model (app_data->tree_view); gint slist_len = 0; gboolean valid = gtk_tree_model_get_iter_first (model, &iter); while (valid) { GtkTreePath *path = gtk_tree_model_get_path (model, &iter); gtk_tree_model_get (model, &iter, COLUMN_POSITION_IN_DB, ¤t_db_pos, -1); if (gtk_tree_path_get_indices (path)[0] != current_db_pos) { NodeInfo *node_info = g_new0 (NodeInfo, 1); json_t *obj = json_array_get (app_data->db_data->json_data, current_db_pos); node_info->newpos = gtk_tree_path_get_indices (path)[0]; node_info->hash = json_object_get_hash (obj); nodes_order_slist = g_slist_append (nodes_order_slist, g_memdupX (node_info, sizeof (NodeInfo))); slist_len++; g_free (node_info); } gtk_tree_path_free (path); valid = gtk_tree_model_iter_next(model, &iter); } // move the reordered items to their new position in the database gsize index; json_t *obj; for (gint i = 0; i < slist_len; i++) { NodeInfo *ni = g_slist_nth_data (nodes_order_slist, i); json_array_foreach (app_data->db_data->json_data, index, obj) { guint32 db_obj_hash = json_object_get_hash (obj); if (db_obj_hash == ni->hash) { // remove the obj from the current position... json_incref (obj); json_array_remove (app_data->db_data->json_data, index); // ...and add it to the desired one json_array_insert (app_data->db_data->json_data, ni->newpos, obj); json_decref (obj); } } g_free (ni); } // update the database and reload the changes GError *err = NULL; update_and_reload_db (app_data, app_data->db_data, TRUE, &err); if (err != NULL) { gchar *msg = g_strconcat ("[ERROR] Failed to update_and_reload_db: ", err->message, NULL); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); g_clear_error (&err); } g_slist_free (nodes_order_slist); } static void hide_all_otps_cb (GtkTreeView *tree_view, gpointer user_data) { gtk_tree_model_foreach (GTK_TREE_MODEL(gtk_tree_view_get_model (tree_view)), clear_all_otps, user_data); } static gboolean clear_all_otps (GtkTreeModel *model, GtkTreePath *path __attribute__((unused)), GtkTreeIter *iter, gpointer user_data __attribute__((unused))) { gchar *otp; gtk_tree_model_get (model, iter, COLUMN_OTP, &otp, -1); if (otp != NULL && g_utf8_strlen (otp, -1) > 4) { gtk_list_store_set (GTK_LIST_STORE(model), iter, COLUMN_OTP, "", COLUMN_VALIDITY, 0, COLUMN_UPDATED, FALSE, COLUMN_LESS_THAN_A_MINUTE, FALSE, -1); } g_free (otp); // do not stop walking the store, check next row return FALSE; } static void set_json_data (json_t *array, ParsedData *pjd) { gsize array_len = json_array_size (array); pjd->types = (gchar **) g_malloc0 ((array_len + 1) * sizeof (gchar *)); pjd->labels = (gchar **) g_malloc0 ((array_len + 1) * sizeof (gchar *)); pjd->issuers = (gchar **) g_malloc0 ((array_len + 1) * sizeof (gchar *)); pjd->periods = g_array_new (FALSE, FALSE, sizeof(gint)); for (guint i = 0; i < array_len; i++) { json_t *obj = json_array_get (array, i); pjd->types[i] = g_strdup (json_string_value (json_object_get (obj, "type"))); pjd->labels[i] = g_strdup (json_string_value (json_object_get (obj, "label"))); pjd->issuers[i] = g_strdup (json_string_value (json_object_get (obj, "issuer"))); json_int_t period = json_integer_value (json_object_get (obj, "period")); g_array_append_val (pjd->periods, period); } pjd->types[array_len] = NULL; pjd->labels[array_len] = NULL; pjd->issuers[array_len] = NULL; } static void add_data_to_model (DatabaseData *db_data, GtkListStore *store) { GtkTreeIter iter; ParsedData *pjd = g_new0 (ParsedData, 1); set_json_data (db_data->json_data, pjd); gint i = 0; while (pjd->types[i] != NULL) { gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, COLUMN_TYPE, pjd->types[i], COLUMN_ACC_LABEL, pjd->labels[i], COLUMN_ACC_ISSUER, pjd->issuers[i], COLUMN_PERIOD, g_array_index (pjd->periods, gint, i), COLUMN_UPDATED, FALSE, COLUMN_LESS_THAN_A_MINUTE, FALSE, COLUMN_POSITION_IN_DB, i, -1); i++; } free_pjd (pjd); } static void add_columns (GtkTreeView *tree_view) { GtkCellRenderer *renderer = gtk_cell_renderer_text_new (); GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes ("Type", renderer, "text", COLUMN_TYPE, NULL); gtk_tree_view_append_column (tree_view, column); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("Account", renderer, "text", COLUMN_ACC_LABEL, NULL); gtk_tree_view_column_set_sizing (GTK_TREE_VIEW_COLUMN(column), GTK_TREE_VIEW_COLUMN_AUTOSIZE); gtk_tree_view_append_column (tree_view, column); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("Issuer", renderer, "text", COLUMN_ACC_ISSUER, NULL); gtk_tree_view_column_set_sizing (GTK_TREE_VIEW_COLUMN(column), GTK_TREE_VIEW_COLUMN_AUTOSIZE); gtk_tree_view_append_column (tree_view, column); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("OTP Value", renderer, "text", COLUMN_OTP, NULL); gtk_tree_view_column_set_sizing (GTK_TREE_VIEW_COLUMN(column), GTK_TREE_VIEW_COLUMN_AUTOSIZE); gtk_tree_view_append_column (tree_view, column); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("Validity", renderer, "text", COLUMN_VALIDITY, NULL); gtk_tree_view_column_set_sizing (GTK_TREE_VIEW_COLUMN(column), GTK_TREE_VIEW_COLUMN_AUTOSIZE); gtk_tree_view_append_column (tree_view, column); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("Period", renderer, "text", COLUMN_PERIOD, NULL); gtk_tree_view_column_set_visible (column, FALSE); gtk_tree_view_append_column (tree_view, column); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("Updated", renderer, "text", COLUMN_UPDATED, NULL); gtk_tree_view_column_set_visible (column, FALSE); gtk_tree_view_append_column (tree_view, column); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("Less Than a Minute", renderer, "text", COLUMN_LESS_THAN_A_MINUTE, NULL); gtk_tree_view_column_set_visible (column, FALSE); gtk_tree_view_append_column (tree_view, column); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes ("Position in Database", renderer, "text", COLUMN_POSITION_IN_DB, NULL); gtk_tree_view_column_set_visible (column, FALSE); gtk_tree_view_append_column (tree_view, column); } static void free_pjd (ParsedData *pjd) { g_strfreev (pjd->types); g_strfreev (pjd->labels); g_strfreev (pjd->issuers); g_array_free (pjd->periods, TRUE); g_free (pjd); } OTPClient-3.2.1/src/treeview.h000066400000000000000000000015721452112020400161040ustar00rootroot00000000000000#pragma once #include "data.h" G_BEGIN_DECLS enum { COLUMN_TYPE, COLUMN_ACC_LABEL, COLUMN_ACC_ISSUER, COLUMN_OTP, COLUMN_VALIDITY, COLUMN_PERIOD, COLUMN_UPDATED, COLUMN_LESS_THAN_A_MINUTE, COLUMN_POSITION_IN_DB, NUM_COLUMNS }; void create_treeview (AppData *app_data); void update_model (AppData *app_data); void delete_rows_cb (GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data); void row_selected_cb (GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data); void reorder_db (AppData *app_data); G_END_DECLS OTPClient-3.2.1/src/ui/000077500000000000000000000000001452112020400145115ustar00rootroot00000000000000OTPClient-3.2.1/src/ui/otpclient.ui000066400000000000000000004076171452112020400170700ustar00rootroot00000000000000 False True False vertical True True True add_menu.import_qr_file From file False True 0 True True True add_menu.import_qr_clipboard From clipboard False True 2 import_qr_menu 1 True False vertical True True True add_menu.webcam Scan using webcam False True 0 True True True Using a QR Code import_qr_menu False True 2 True True True add_menu.manual Manually False True 3 main 2 False 5 Change database center dialog center False vertical False end Cancel True True True True True 0 OK True True True True True 1 False False 0 True False vertical 5 True False Use the rightmost icon on the second entry to select a different OTPClient database. True center True False True 0 True False True Max 255 chars False 255 Show password False True 5 1 True True Max 255 chars False 255 document-open-symbolic Open database file Absolute path of the existing database... False True 2 False True 1 changedb_diag_cancel_btn_id changedb_diag_ok_btn_id False 5 Change password center dialog center False vertical 5 False end Cancel True True True True True 0 OK True True True True True 1 False False 0 True False vertical 5 True False Please type the current password, the new one, and again the new one to verify its correctness. Please note that <b>there is no way to recover a forgotten password.</b> True center True False True 0 True True Max 255 chars 255 False dialog-password-symbolic Show password Type current password... password False True 1 True True Max 255 chars 255 False dialog-password-symbolic Show password Type new password... password False True 2 True True Max 255 chars 255 False dialog-password-symbolic Show password Retype new password... password False True 3 False True 1 changepwd_diag_cancel_btn_id changepwd_diag_ok_btn_id False 2 Database info center dialog center False vertical 2 False end Close True True True True True 0 False False 0 True False 5 10 2 True False Database location 0 0 True False Config file location 0 1 True False Entries in the database 0 2 True False Encryption algo 0 3 True False KDF iterations 0 4 True True True False 1 0 True True True False 1 1 True False 1 2 True False AES256-GCM 1 3 True False 100'000 1 4 False True 1 dbinfo_closebtn_id False 5 Password center 400 150 dialog center False vertical 10 False end Cancel True True True True True 0 OK True True True True True 1 False False 0 True False 5 5 True center True False True 1 True True Max 255 chars 255 False dialog-password-symbolic Show password Type password... password False True 2 decpwddiag_cancel_btn_id decpwddiag_ok_btn_id del_diag_id False dialog False vertical 2 False end No True True True True True 0 Yes True True True True True 1 False False 0 True False 5 5 Are you sure you want to <b>permanently delete</b> the selected item? True center False True 1 del_diag_no_btn_id del_diag_yes_btn_id False dialog False 5 5 vertical 10 False end Quit True True True True True 0 Select another DB True True True True True 1 Create new DB True True True True True 2 False False 0 True False 5 5 True center False True 0 btn_changefile_quit_id btn_changefile_selex_id btn_changefile_new_id False Scan using webcam center dialog center False 5 5 vertical 5 False end Cancel True True True True True 0 False False 0 True False Select the file with the QR code and copy it (CTRL-C). If you are using <b>KDE</b>, then you must copy the file <b>before</b> selecting this option, otherwise the content won't be parsed. This dialog will close automatically as soon as a valid qrcode has been found or after 30 seconds if nothing has been detected. True center True True True 1 cancel_btn_qrclipb_id False Scan using webcam center dialog center False 5 5 vertical False end Cancel True True True True True 0 False False 0 True False Please place the qrcode <b>in front of the webcam</b>. This dialog will automatically close as soon as a valid qrcode has been found or after 30 seconds if nothing has been detected. True center True True 1 cancel_btn_webcam_id False 5 Database location center dialog center False vertical 2 False end Cancel True True True 15 True True 1 False False 0 True False vertical 6 True False 5 This seems to be the first time you run OTPClient on this device. Please select whether you want to restore an existing database or create a new one. center True False True 0 diag_rc_restoredb_btn True True True Do NOT use to import third party databases (e.g. andOTP) True False Restore existing <b>OTPClient</b> database True False True 1 Create new database diag_rc_createdb_btn True True True False True 2 False True 1 diag_rc_cancel_btn_id 300 False 5 Edit data center dialog center False vertical 5 False end Cancel True True True True True 0 OK True True True True True 1 False False 0 True False 5 Please note that: - "account" is <b>mandatory</b> - "issuer", while optional, is <b>highly recommended</b> - when changing the "secret", be aware that <b>only the secret</b> itself can be changed, but not the number of digits and/or the period/counter. True True False True 1 True False vertical 8 True False 0 in True False 7 12 10 10 True False 5 True True False Check if you want to edit the "Account" True False True 0 True True False 64 True True 1 True False Account False True 0 True False 0 in True False 7 12 10 10 True False 5 True True False Check if you want to edit the "Issuer" True False True 0 True True False 64 True True 1 True False Issuer False True 1 True False 0 in True False 7 12 10 10 True False 5 True True False Check if you want to edit the "Secret" True False True 0 True True False 255 False dialog-password-symbolic True True 1 True False Secret False True 2 True True 2 edit_diag_cancel_btn edit_diag_ok_btn True False mail-send-receive True False list-remove-symbolic False 10 Add Token center 500 dialog center False vertical 10 False end Cancel True True True True True 0 OK True True True True True 1 False False 0 True False vertical 5 True False 2 otp_cb True False 0 TOTP HOTP True True 0 True False 0 SHA1 SHA256 SHA512 True True 1 This is a Steam code True True False True False True 2 False True 0 True True 64 Label Account True True 1 True True 64 Issuer Issuer True True 2 True True 255 False dialog-password-symbolic Secret Secret password True True 3 True False 2 2 True True False vertical True False Digits False True 0 True True Between 4 and 10 10 10 6 False digits False True 1 False True 0 True False vertical True False Period False True 0 True True In seconds between 10 and 120 3 10 30 False digits False True 1 False True 1 True False vertical True False Counter False True 0 True True Value decided by the server (HOTP only) 10 False digits False True 1 False True 2 False True 4 False True 1 manual_diag_cancel_btn_id manual_diag_ok_btn_id False 5 New empty database center dialog center False vertical False end Cancel True True True True True 0 OK True True True True True 1 False False 0 True False vertical 5 True False This will create and load a new empty database. The current database will <b>neither</b> be overwritten <b>nor</b> deleted. True center True False True 0 True True Max 255 chars False 255 document-open-symbolic Open database file Absolute path of the new database... False True 1 False True 1 newdb_diag_cancel_btn_id newdb_diag_ok_btn_id False 5 Password center dialog center False vertical 5 False end Cancel True True True True True 0 OK True True True True True 1 False False 0 True False vertical 5 True False Choose an encryption password for the database. Please note that <b>there is no way to recover a forgotten password.</b> True center True False True 0 True True Max 255 chars 255 False dialog-password-symbolic Show password Type password... password False True 1 True True Max 255 chars 255 False dialog-password-symbolic Show password Retype password... password False True 2 False True 1 newdb_pwd_diag_cancel_btn_id newdb_pwd_diag_ok_btn_id False center dialog center False vertical 5 False end OK True True True True True 1 False False 0 True False True True 1 qr_code_diag_ok_btn_id False 5 Settings center dialog center False vertical 10 False end Cancel True True True True True 0 OK True True True True True 1 False False 0 True False vertical 10 10 True False Show next OTP 0 0 True False Disable notifications 0 1 True False Search by 0 2 True True center center 1 1 True False 0 0 Account Issuer 1 2 True True center center 1 0 True False Auto lock on system lock 0 3 True True center center 1 3 True False Auto lock when inactive for 0 4 True False 0 0 Never 30s 1m 5m 15m 30m 1 4 True False Dark theme enabled 0 5 True True center center 1 5 True False Enable secret service 0 6 True True center center 1 6 False True 1 settings_diag_cancel_btn_id settings_diag_ok_btn_id False True False vertical True True True settings_menu.import_andotp andOTP (encrypted) False True 0 True True True settings_menu.import_andotp_plain andOTP (plain) False True 1 True True True settings_menu.import_authplus Authenticator Plus False True 2 True True True settings_menu.import_freeotpplus FreeOTP+ (key URI) False True 3 True True True settings_menu.import_aegis Aegis (plain json) False True 4 True True True settings_menu.import_aegis_enc Aegis (encrypted json) False True 5 True True True Google Migration QR import_google_qr_menu False True 6 import_menu 1 True False vertical True True True settings_menu.export_andotp andOTP (encrypted) False True 0 True True True settings_menu.export_andotp_plain andOTP (plain) False True 1 True True True settings_menu.export_freeotpplus FreeOTP+ (key URI) False True 2 True True True settings_menu.export_aegis Aegis (encrypted json) False True 3 True True True settings_menu.export_aegis_plain Aegis (plain json) False True 4 export_menu 2 True False vertical True True True Import import_menu False True 0 True True True Export export_menu False True 1 True True True settings_menu.create_newdb New database False True 2 True True True settings_menu.change_db Change database False True 3 True True True settings_menu.change_pwd Change password False True 4 True True True settings_menu.edit_row Edit row False True 5 True True True settings_menu.show_qr Show QR-Code False True 6 True True True settings_menu.settings Settings False True 7 True True True settings_menu.shortcuts Keyboard shortcuts False True 8 True True True settings_menu.dbinfo Database info False True 9 True True True settings_menu.about About False True 10 main 3 True False vertical True True True settings_menu.import_google_qr_file From file False True 0 True True True settings_menu.import_google_qr_webcam From webcam False True 1 import_google_qr_menu 4 False center 500 350 center True False vertical False vertical False 6 end OK True True True True True 0 False False 0 False 16 True False False True 0 False False 0 info_bar_ok_btn_id False False 0 True True True True etched-in True True True True False horizontal True False True 1 True False OTPClient False True True False True True True Add token add_pop_id True False list-add-symbolic False True 0 delbtn True True True Toggle delete rows lrs_image False True 1 True True True Enable/disable rows reordering image1 False True 2 True True True Settings settings_pop_id True False open-menu-symbolic end 1 True True True True False system-lock-screen-symbolic end 2 False 5 Password center dialog center False vertical 10 False end Quit True True True True True 0 Unlock True True True True True 1 False False 0 True False vertical 10 True False The application is <b>locked</b>. Please type your password to unlock it True center True False True 0 True True Max 255 chars 255 False dialog-password-symbolic Show password Type password... password False True 1 False True 1 unlock_diag_quit_btn_id unlock_diag_unlock_btn_id False Warning: memlock value too low center 400 dialog center False vertical 2 False end Exit True True True True True 0 OK True True True True True 1 False False 0 True False vertical 9 True False True center True True True 0 Do not show this warning again True True False True False True 1 False True 1 warning_diag_exit_btn_id warning_diag_ok_btn_id OTPClient-3.2.1/src/ui/shortcuts.ui000066400000000000000000000135061452112020400171130ustar00rootroot00000000000000 1 1 shortcuts 12 1 Add token add 1 <Alt>w Scan QR code using webcam 1 <Alt>m Manually insert data 1 General general 1 <Ctrl>d Toggle delete rows 1 <Ctrl>r Toggle reorder rows 1 <Ctrl>f Search 1 <Ctrl>l Lock 1 <Ctrl>q Quit 1 <Ctrl>s Show settings 1 <Ctrl>k Show keyboard shortcuts 1 Menu settings 1 <Alt>h Hide all otps 1 <Alt>e Edit selected row 1 <Alt>q Show QR-Code 1 <Ctrl>b Change database 1 <Ctrl>o Change password OTPClient-3.2.1/src/webcam-add-cb.c000066400000000000000000000074111452112020400166110ustar00rootroot00000000000000#include #include #include #include "imports.h" #include "parse-uri.h" #include "message-dialogs.h" #include "get-builder.h" #include "common/common.h" #include "gui-common.h" typedef struct config_data_t { GtkWidget *diag; gchar *otp_uri; gboolean qrcode_found; gboolean gtimeout_exit_value; guint counter; } ConfigData; static gboolean check_result (gpointer data); static void scan_qrcode (zbar_image_t *image, gconstpointer user_data); void webcam_add_cb (GSimpleAction *simple, GVariant *parameter __attribute__((unused)), gpointer user_data) { const gchar *action_name = g_action_get_name (G_ACTION(simple)); gboolean google_migration = (g_strcmp0 (action_name, GOOGLE_MIGRATION_WEBCAM_ACTION_NAME) == 0) ? TRUE : FALSE; AppData *app_data = (AppData *)user_data; ConfigData *cfg_data = g_new0 (ConfigData, 1); GtkBuilder *builder = get_builder_from_partial_path (UI_PARTIAL_PATH); cfg_data->diag = GTK_WIDGET(gtk_builder_get_object (builder, "diag_webcam_id")); cfg_data->qrcode_found = FALSE; cfg_data->gtimeout_exit_value = TRUE; cfg_data->counter = 0; zbar_processor_t *proc = zbar_processor_create (1); zbar_processor_set_config (proc, ZBAR_NONE, ZBAR_CFG_ENABLE, 1); if (zbar_processor_init (proc, "/dev/video0", 1)) { show_message_dialog (app_data->main_window, "Couldn't initialize the webcam", GTK_MESSAGE_ERROR); zbar_processor_destroy (proc); g_free (cfg_data); return; } zbar_processor_set_data_handler (proc, scan_qrcode, cfg_data); zbar_processor_set_visible (proc, 1); zbar_processor_set_active (proc, 1); guint source_id = g_timeout_add (1000, check_result, cfg_data); gtk_widget_show_all (cfg_data->diag); gint response = gtk_dialog_run (GTK_DIALOG (cfg_data->diag)); if (response == GTK_RESPONSE_CANCEL) { if (cfg_data->qrcode_found) { zbar_processor_destroy (proc); gchar *err_msg = parse_uris_migration (app_data, cfg_data->otp_uri, google_migration); if (err_msg != NULL) { show_message_dialog (app_data->main_window, err_msg, GTK_MESSAGE_ERROR); g_free (err_msg); } else { show_message_dialog (app_data->main_window, "QRCode successfully scanned", GTK_MESSAGE_INFO); } gcry_free (cfg_data->otp_uri); } if (cfg_data->gtimeout_exit_value) { // only remove if 'check_result' returned TRUE g_source_remove (source_id); } gtk_widget_destroy (cfg_data->diag); g_free (cfg_data); } g_object_unref (builder); } void webcam_add_cb_shortcut (GtkWidget *w __attribute__((unused)), gpointer user_data) { webcam_add_cb (g_simple_action_new ("webcam", NULL), NULL, user_data); } static gboolean check_result (gpointer data) { ConfigData *cfg_data = (ConfigData *)data; if (cfg_data->qrcode_found || cfg_data->counter > 30) { gtk_dialog_response (GTK_DIALOG (cfg_data->diag), GTK_RESPONSE_CANCEL); cfg_data->gtimeout_exit_value = FALSE; return FALSE; } cfg_data->counter++; return TRUE; } static void scan_qrcode (zbar_image_t *image, gconstpointer user_data) { ConfigData *cfg_data = (ConfigData *)user_data; const zbar_symbol_t *symbol = zbar_image_first_symbol (image); for (; symbol; symbol = zbar_symbol_next (symbol)) { gchar *unesc_str = g_uri_unescape_string_secure (zbar_symbol_get_data (symbol), NULL); cfg_data->otp_uri = secure_strdup (unesc_str); cfg_data->qrcode_found = TRUE; gcry_free (unesc_str); } } OTPClient-3.2.1/src/webcam-add-cb.h000066400000000000000000000005151452112020400166140ustar00rootroot00000000000000#pragma once #include G_BEGIN_DECLS void webcam_add_cb (GSimpleAction *simple, GVariant *parameter, gpointer user_data); void webcam_add_cb_shortcut (GtkWidget *w, gpointer user_data); G_END_DECLS