├── .clang-format ├── .github └── workflows │ ├── ci-workflow.yml │ ├── codeql-workflow.yml │ ├── guidelines_enforcer.yml │ ├── lint-workflow.yml │ └── wui.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── clients └── wui │ ├── README.md │ ├── package.json │ ├── public │ ├── CNAME │ ├── index.html │ ├── logo.svg │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── components │ │ ├── AppExplanations │ │ │ ├── index.css │ │ │ └── index.js │ │ ├── BackupButton │ │ │ ├── index.css │ │ │ └── index.js │ │ └── RestoreButton │ │ │ ├── index.css │ │ │ └── index.js │ ├── controller │ │ └── PasswordsManager.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ └── setupTests.js │ └── yarn.lock ├── glyphs ├── icon_back.gif ├── icon_backspace.gif ├── icon_backspace_invert.gif ├── icon_bootloader.gif ├── icon_certificate.gif ├── icon_classes.gif ├── icon_classes_invert.gif ├── icon_coggle.gif ├── icon_crossmark.gif ├── icon_dashboard.gif ├── icon_digits.gif ├── icon_digits_invert.gif ├── icon_down.gif ├── icon_eye.gif ├── icon_left.gif ├── icon_lowercase.gif ├── icon_lowercase_invert.gif ├── icon_plus.gif ├── icon_right.gif ├── icon_up.gif ├── icon_uppercase.gif ├── icon_uppercase_invert.gif ├── icon_validate.gif ├── icon_validate_14.gif ├── icon_validate_invert.gif ├── icon_warning.gif └── stax_icon_password_manager_64px.gif ├── icons ├── nanos_icon_password_manager.gif ├── nanox_icon_password_manager.gif └── stax_icon_password_manager_32px.gif ├── include ├── ctr_drbg.h ├── hid_mapping.h ├── password_generation.h └── usbd_hid_impl.h ├── ledger_app.toml ├── pytest.ini ├── src ├── apdu_handlers │ ├── dump_metadatas.c │ ├── get_app_config.c │ ├── handlers.h │ └── load_metadatas.c ├── app_main.c ├── ctaes │ ├── COPYING │ ├── README.md │ ├── ctaes.c │ └── ctaes.h ├── ctr_drbg.c ├── dispatcher.c ├── dispatcher.h ├── error.c ├── error.h ├── globals.c ├── globals.h ├── hid_mapping.c ├── keyboards │ ├── bolos_ux_nanos_keyboard.c │ ├── bolos_ux_nanox_keyboard.c │ ├── keyboard.h │ ├── keyboard_common.c │ └── text_keyboard.c ├── metadata.c ├── metadata.h ├── nano │ └── ui.c ├── options.c ├── options.h ├── password.c ├── password.h ├── password_generation.c ├── password_typing.c ├── password_typing.h ├── stax │ ├── password_list.c │ ├── password_list.h │ └── ui.c ├── tests │ ├── tests.c │ └── tests.h ├── types.h └── ui.h └── tests ├── functional ├── automation.json ├── conftest.py ├── exception │ ├── __init__.py │ ├── device_exception.py │ └── types.py ├── passwordsManager_cmd.py ├── requirements.txt ├── snapshots │ └── stax │ │ ├── all_passwords_deleted_screen.png │ │ ├── confirm_all_passwords_deletion.png │ │ ├── confirm_password_deletion.png │ │ ├── create_password │ │ ├── 00000.png │ │ ├── 00001.png │ │ ├── 00002.png │ │ ├── 00003.png │ │ ├── 00004.png │ │ ├── 00005.png │ │ ├── 00006.png │ │ ├── 00007.png │ │ ├── 00008.png │ │ ├── 00009.png │ │ ├── 00010.png │ │ ├── 00011.png │ │ └── 00012.png │ │ ├── delete_all_password │ │ ├── 00000.png │ │ ├── 00001.png │ │ ├── 00002.png │ │ ├── 00003.png │ │ ├── 00004.png │ │ ├── 00005.png │ │ ├── 00006.png │ │ └── 00007.png │ │ ├── delete_one_password │ │ ├── 00000.png │ │ ├── 00001.png │ │ ├── 00002.png │ │ ├── 00003.png │ │ ├── 00004.png │ │ ├── 00005.png │ │ ├── 00006.png │ │ ├── 00007.png │ │ ├── 00008.png │ │ └── 00009.png │ │ ├── disclaimer.png │ │ ├── home_screen.png │ │ ├── keyboard_screen_empty.png │ │ ├── keyboard_screen_n_text.png │ │ ├── keyboard_screen_ne_text.png │ │ ├── keyboard_screen_new_text.png │ │ ├── list_screen_empty.png │ │ ├── list_screen_populated.png │ │ ├── list_screen_populated_and_new.png │ │ ├── list_screen_populated_one_deleted.png │ │ ├── menu_screen.png │ │ ├── password_created_screen.png │ │ ├── password_deleted_screen.png │ │ ├── settings │ │ ├── 00000.png │ │ ├── 00001.png │ │ ├── 00002.png │ │ ├── 00003.png │ │ ├── 00004.png │ │ ├── 00005.png │ │ ├── 00006.png │ │ └── 00007.png │ │ └── startup_choose_kbl.png ├── stax │ ├── __init__.py │ ├── navigator.py │ ├── screen.py │ ├── test_common.py │ ├── test_passwords.py │ └── test_settings.py ├── test_cmd.py ├── test_error_cmd.py └── tests_vectors.py └── unit ├── CMakeLists.txt ├── mocks └── os.h ├── stax └── test_password_list.c └── test_hid_mapping.c /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | IndentWidth: 4 4 | --- 5 | Language: Cpp 6 | ColumnLimit: 100 7 | PointerAlignment: Right 8 | AlignAfterOpenBracket: Align 9 | AlignConsecutiveMacros: true 10 | AllowAllParametersOfDeclarationOnNextLine: false 11 | SortIncludes: false 12 | SpaceAfterCStyleCast: true 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowAllArgumentsOnNextLine: false 15 | AllowAllParametersOfDeclarationOnNextLine: false 16 | AllowShortBlocksOnASingleLine: Never 17 | AllowShortFunctionsOnASingleLine: None 18 | BinPackArguments: false 19 | BinPackParameters: false 20 | --- 21 | -------------------------------------------------------------------------------- /.github/workflows/ci-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Compilation & tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | - develop 9 | pull_request: 10 | 11 | jobs: 12 | unittesting: 13 | name: C unit testing 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Clone 17 | uses: actions/checkout@v4 18 | - name: Install cmocka 19 | run: | 20 | sudo apt update 21 | sudo apt install libcmocka-dev lcov libbsd-dev 22 | - name: Compile the tests 23 | run: | 24 | cd tests/unit/ 25 | rm -rf build/ 26 | cmake -B build -H. 27 | make -C build 28 | - name: Run the tests 29 | run: | 30 | cd tests/unit/ 31 | CTEST_OUTPUT_ON_FAILURE=1 make -C build test 32 | - name: Generate code coverage 33 | run: | 34 | cd tests/unit/ 35 | lcov --directory . -b "$(realpath build/)" --capture --initial -o coverage.base 36 | lcov --rc lcov_branch_coverage=1 --directory . -b "$(realpath build/)" --capture -o coverage.capture 37 | lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info 38 | lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/unit/*' -o coverage.info 39 | genhtml coverage.info -o coverage 40 | - uses: actions/upload-artifact@v4 41 | with: 42 | name: code-coverage 43 | path: tests/unit/coverage 44 | 45 | build: 46 | name: Build application using the reusable workflow 47 | uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_build.yml@v1 48 | with: 49 | flags: "TESTING=1 POPULATE=1" 50 | upload_app_binaries_artifact: apps 51 | 52 | test: 53 | name: Test the application using the reusable workflow 54 | uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_ragger_tests.yml@v1 55 | needs: build 56 | with: 57 | download_app_binaries_artifact: apps 58 | -------------------------------------------------------------------------------- /.github/workflows/codeql-workflow.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | pull_request: 9 | # Excluded path: add the paths you want to ignore instead of deleting the workflow 10 | paths-ignore: 11 | - '.github/workflows/*.yml' 12 | - 'tests/*' 13 | 14 | jobs: 15 | analyse: 16 | name: Analyse 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - SDK: "$NANOS_SDK" 22 | name: nanos 23 | - SDK: "$NANOX_SDK" 24 | name: nanox 25 | - SDK: "$NANOSP_SDK" 26 | name: nanos2 27 | - SDK: "$STAX_SDK" 28 | name: stax 29 | #'cpp' covers C and C++ 30 | language: [ 'cpp' ] 31 | runs-on: ubuntu-latest 32 | container: 33 | image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest 34 | 35 | steps: 36 | - name: Clone 37 | uses: actions/checkout@v4 38 | 39 | - name: Initialize CodeQL 40 | uses: github/codeql-action/init@v2 41 | with: 42 | languages: ${{ matrix.language }} 43 | queries: security-and-quality 44 | 45 | # CodeQL will create the database during the compilation 46 | - name: Build 47 | run: | 48 | make BOLOS_SDK=${{ matrix.SDK }} TARGET=${{ matrix.name }} 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v2 52 | -------------------------------------------------------------------------------- /.github/workflows/guidelines_enforcer.yml: -------------------------------------------------------------------------------- 1 | name: Ensure compliance with Ledger guidelines 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | - main 9 | - develop 10 | pull_request: 11 | 12 | jobs: 13 | guidelines_enforcer: 14 | name: Call Ledger guidelines_enforcer 15 | uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_guidelines_enforcer.yml@v1 16 | -------------------------------------------------------------------------------- /.github/workflows/lint-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Code style check 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | paths-ignore: 10 | - '.github/workflows/*.yml' 11 | - 'tests/*' 12 | 13 | jobs: 14 | job_lint: 15 | name: Lint 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Clone 20 | uses: actions/checkout@v4 21 | 22 | - name: Lint 23 | uses: DoozyX/clang-format-lint-action@v0.14 24 | with: 25 | source: './src ./include/' 26 | extensions: 'h,c' 27 | clangFormatVersion: 11 28 | 29 | misspell: 30 | name: Check misspellings 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - name: Clone 35 | uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Check misspellings 40 | uses: codespell-project/actions-codespell@master 41 | with: 42 | builtin: clear,rare 43 | check_filenames: true 44 | ignore_words_list: ontop 45 | skip: ./clients/wui/yarn.lock, 46 | -------------------------------------------------------------------------------- /.github/workflows/wui.yml: -------------------------------------------------------------------------------- 1 | name: Password backup site generation & update 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - develop 8 | - master 9 | pull_request: 10 | branches: 11 | - develop 12 | - master 13 | 14 | jobs: 15 | generate: 16 | name: Generate the site 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Clone 20 | uses: actions/checkout@v4 21 | - name: Set Node.js 16.x 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 16.x 25 | - name: Run install 26 | uses: borales/actions-yarn@v4 27 | with: 28 | cmd: install 29 | dir: clients/wui/ 30 | - name: Run install 31 | uses: borales/actions-yarn@v4 32 | with: 33 | cmd: build 34 | dir: clients/wui/ 35 | - name: Upload documentation bundle 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: site 39 | path: clients/wui/build/ 40 | 41 | deploy: 42 | name: Deploy the site on Github pages 43 | runs-on: ubuntu-latest 44 | needs: generate 45 | if: | 46 | github.event_name == 'push' && 47 | (github.ref == 'refs/heads/master' || 48 | github.ref == 'refs/heads/develop' || 49 | startsWith(github.ref, 'refs/tags/')) 50 | steps: 51 | - name: Download documentation bundle 52 | uses: actions/download-artifact@v4 53 | - name: Deploy documentation on pages 54 | uses: peaceiris/actions-gh-pages@v3 55 | with: 56 | github_token: ${{ secrets.GITHUB_TOKEN }} 57 | publish_dir: site 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | debug 3 | dep 4 | build 5 | obj 6 | src/glyphs.c 7 | src/glyphs.h 8 | *.pyc 9 | .python-version 10 | *~ 11 | speculos.log 12 | scan-build 13 | 14 | tests/functional/elfs 15 | coverage* 16 | snapshots-tmp/ 17 | 18 | node_modules 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.1.3] - 2024-??-?? 9 | 10 | ### Fix 11 | 12 | - Bug #38: some applications were randomly lowering capital characters, leading passwords containing 13 | capital characters to be almost always wrong. 14 | 15 | ## [1.1.2] - 2023-10-12 (Stax only) 16 | 17 | ### Fix 18 | 19 | - Updated porting to Stax SDK evolutions 20 | 21 | ## [1.1.1] - 2023-04-25 (Stax only) 22 | 23 | ### Fix 24 | 25 | - Derivation path changed, from `44'/1` to `5265220'` 26 | 27 | ## [1.1.0] - 2023-04-12 (Stax only) 28 | 29 | ### Add 30 | 31 | - Stax porting 32 | 33 | ## [1.0.2] - 2022-03-02 34 | 35 | Original Passwords application 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #******************************************************************************* 2 | # Ledger App 3 | # (c) 2017 Ledger 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | #******************************************************************************* 17 | 18 | ifeq ($(BOLOS_SDK),) 19 | $(error Environment variable BOLOS_SDK is not set) 20 | endif 21 | include $(BOLOS_SDK)/Makefile.defines 22 | 23 | all: default 24 | 25 | APPNAME ="Passwords" 26 | APPVERSION_M=1 27 | APPVERSION_N=2 28 | APPVERSION_P=0 29 | APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P) 30 | 31 | VARIANT_PARAM = NONE 32 | VARIANT_VALUES = pwmgr 33 | 34 | CURVE_APP_LOAD_PARAMS = secp256k1 35 | PATH_APP_LOAD_PARAMS = "5265220'" 36 | HAVE_APPLICATION_FLAG_GLOBAL_PIN = 1 37 | 38 | DISABLE_OS_IO_STACK_USE=1 39 | DISABLE_STANDARD_U2F=1 40 | 41 | DEFINES += APPNAME=\"$(APPNAME)\" 42 | 43 | ICON_NANOS = icons/nanos_icon_password_manager.gif 44 | ICON_NANOSP = icons/nanox_icon_password_manager.gif 45 | ICON_NANOX = icons/nanox_icon_password_manager.gif 46 | ICON_STAX = icons/stax_icon_password_manager_32px.gif 47 | 48 | DEFINES += OS_IO_SEPROXYHAL 49 | DEFINES += HAVE_IO_USB HAVE_L4_USBLIB IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 HAVE_USB_APDU 50 | DEFINES += MAX_METADATAS=4096 MAX_METANAME=20 51 | DEFINES += USE_CTAES 52 | DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=0 WEBUSB_URL="" 53 | DEFINES += HAVE_SPRINTF 54 | 55 | TESTING ?= 0 56 | ifeq ($(TESTING), 0) 57 | $(info TESTING DISABLED) 58 | DEFINES += HAVE_USB_HIDKBD 59 | else 60 | $(info TESTING ENABLED) 61 | DEFINES += TESTING 62 | endif 63 | 64 | ifneq ($(TARGET_NAME), TARGET_STAX) 65 | $(info Using BAGL) 66 | DEFINES += HAVE_BAGL 67 | DEFINES += HAVE_UX_FLOW 68 | ifneq ($(TARGET_NAME), TARGET_NANOS) 69 | DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300 70 | DEFINES += HAVE_GLO096 71 | DEFINES += BAGL_WIDTH=128 BAGL_HEIGHT=64 72 | DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature 73 | DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX 74 | DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX 75 | DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX 76 | else 77 | DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128 78 | endif 79 | else 80 | $(info Using NBGL) 81 | DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300 82 | DEFINES += NBGL_KEYBOARD 83 | endif 84 | 85 | POPULATE ?= 0 86 | ifeq ($(POPULATE), 0) 87 | $(info POPULATE DISABLED) 88 | else 89 | $(info POPULATE ENABLED) 90 | DEFINES += POPULATE 91 | endif 92 | 93 | ### computed variables 94 | APP_SOURCE_PATH += src 95 | 96 | #add dependency on custom makefile filename 97 | dep/%.d: %.c Makefile 98 | 99 | include $(BOLOS_SDK)/Makefile.standard_app 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # app-passwords 2 | 3 | ## Quick summary 4 | 5 | The Passwords application for Ledger Nano S and Nano X is available for download on the [Ledger Live](https://www.ledger.com/ledger-live/download). 6 | 7 | This application demonstrates a Password Manager implemented with no support from the host - the passwords are typed from the Nano S interacting as a keyboard to the connected computer / phone. 8 | 9 | ## Usage 10 | 11 | To create a password: 12 | 13 | - Choose which kind of characters you want in this password (lowercase, uppercase, numbers, dashes, extra symbols) 14 | - Enter a nickname for the new entry (for instance, "wikipedia.com"). 15 | The device then derives a deterministic password from the device's seed and this nickname. 16 | 17 | To type a password, just select it in your list of password. 18 | 19 | If you want to add a lot of passwords, this process can be pretty painful. Instead of doing it manually, you can use the [backup tool](https://blog.ledger.com/passwords-backup/) to load a custom list of password nicknames. 20 | 21 | ### Application settings 22 | 23 | In the application settings, the user can configure 24 | 25 | - Which keyboard the device should emulate when typing a password (Qwerty, International Qwerty or Azerty). 26 | 27 | - If the `Enter` key should be pressed automatically after typing a password (this is convenient for typing start-up password at encrypted servers without attaching display and keyboard, for instance). 28 | 29 | ## Backup 30 | 31 | As passwords are deterministically derived, it's not a problem if you loose your device, as long as you remember the password nicknames and you still have you device recovery phrase to set up again the Passwords app on a new device. 32 | 33 | Same applies when updating the device firmware or the application itself, the list of password nicknames won't be restored automatically, so make sure to save a backup using [this tool](https://blog.ledger.com/passwords-backup/). 34 | 35 | These nicknames are not confidential (meaning, someone who finds them will not be able to retrieve your passwords without your [24-words recovery phrase](https://www.ledger.com/academy/crypto/what-is-a-recovery-phrase)), so you don't have to hide your backup like you did with your recovery phrase. Sending it to yourself by e-mail is fine. 36 | 37 | ## Password generation mechanism 38 | 39 | - Metadatas are SHA-256 hashed 40 | 41 | - The SHA-256 components are turned into 8 big endian uint32 | 0x80000000 42 | 43 | - A private key and chain code are derived for secp256k1 over 0x80505744 / the path computed before 44 | 45 | - The private key and chain code are SHA-256 hashed, the result is used as the entropy to seed an AES DRBG 46 | 47 | - A password is generated by randomly choosing from a set of characters using the previously seeded DRBG 48 | 49 | ## Troobleshooting 50 | 51 | - If you configured a password with some charset only, and you get unwanted characters when typing it, check that you have configured the application with the right keyboard. It must be configured like the keyboard settings of your operating systems to type correctly. 52 | 53 | - If the keyboard is not recognized by your computer, have a look [here](https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues) 54 | 55 | ## Tests 56 | 57 | ### Unit 58 | 59 | #### Prerequisite 60 | 61 | Be sure to have installed: 62 | 63 | - CMake >= 3.10 64 | - CMocka >= 1.1.5 65 | 66 | and for code coverage generation: 67 | 68 | - lcov >= 1.14 69 | 70 | #### Overview 71 | 72 | Unit tests are in `C` and uses `cmake` to build and `cmocka` as a library. 73 | You will then need to compile the tests: 74 | 75 | ```bash 76 | (cd tests/unit/ && \ 77 | rm -rf build/ && \ 78 | cmake -B build -H. && \ 79 | make -C build) 80 | ``` 81 | 82 | You can then run the tests: 83 | 84 | ```bash 85 | (cd tests/unit/ && \ 86 | CTEST_OUTPUT_ON_FAILURE=1 make -C build test) 87 | ``` 88 | 89 | ### Functional 90 | 91 | Functional tests are written with Pytest. Before running them, you first need to compile the application with env variables `TESTING=1` and `POPULATE=1`: 92 | 93 | ```bash 94 | make all TESTING=1 POPULATE=1 95 | ``` 96 | 97 | Then you can execute tests on speculos with: 98 | 99 | ```bash 100 | pytest tests/functional 101 | ``` 102 | 103 | To run tests on a real device, load the app on it: 104 | 105 | ```bash 106 | make load TESTING=1 POPULATE=1 107 | ``` 108 | 109 | Then open the app on your device and run: 110 | 111 | ```bash 112 | pytest --hid 113 | ``` 114 | 115 | ## Future work 116 | 117 | This release is an early alpha - among the missing parts : 118 | 119 | - Support of different password policies mechanisms 120 | 121 | ## Credits 122 | 123 | This application uses 124 | 125 | - MBED TLS AES DRBG implementation (https://tls.mbed.org/ctr-drbg-source-code) 126 | -------------------------------------------------------------------------------- /clients/wui/README.md: -------------------------------------------------------------------------------- 1 | # Passwords Backup 2 | 3 | [Live demo here](https://passwords.ledger.com) 4 | 5 | ## What is this Web App ? 6 | 7 | This Web App allows you to backup/restore the list of `password nicknames` stored inside the `Passwords app` on your Ledger Nano S/ Nano X. 8 | It is useful to have such a backup when you update the Passwords app on your device, or the device firmware, because the list gets erased. 9 | 10 | Another case where it's practical to have a nickname backup is when you loose your device: Restoring the [24-words recovery phrase](https://www.ledger.com/academy/crypto/what-is-a-recovery-phrase) is necessary but not sufficient to restore your passwords, you need your nickname list as well. 11 | 12 | The backup consists in a human readable `backup.json` file containing a dump of the 4096 bytes of application storage. 13 | 14 | Note that all operations of this Web App are done locally on your computer, there are no external communications occurring. 15 | 16 | ## What is the Ledger Passwords application ? 17 | 18 | Look [here](https://github.com/LedgerHQ/app-passwords/blob/master/README.md) for more information on the device application itself. 19 | 20 | ## How to use this Web App ? 21 | 22 | - Install the `Passwords app` on your Nano S/ Nano X from the [Ledger Live](https://support.ledger.com/hc/en-us/articles/360006523674-Install-uninstall-and-update-apps). 23 | - Connect your Nano S/Nano X to your computer and open the `Passwords app`. 24 | - You can now click on the big `Connect` button, and if it succeeds the Backup/Restore buttons should appear in place of the previous button. If you have troubles with this step, have a look [here](https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues). \n\* Either click on Backup/Restore depending on what you want to do. 25 | - `Backup` will prompt a screen requesting your approval on your device (`\"Transfer metadatas ?\"`), then save a backup file. This is your backup. it's not confidential, so for instance you can send it to yourself by e-mail to never loose it. 26 | - `Restore` will prompt a file input dialog where you should indicate a previous backup file. A prompt (`\"Overwrite metadatas ?\"`) will then request your approval on your device. 27 | 28 | ## Which web browsers are supported ? 29 | 30 | The communication with the device is done through `WebUSB`, which is currently supported only on `Google Chrome` / `Chromium` / `Brave` for `Linux` and `MacOS`. On `Windows`, [Zadig](https://github.com/WICG/webusb/issues/143) is required. 31 | 32 | ## Less common use cases 33 | 34 | - If you ever encounter a WTF-kind of error with your passwords app (some or all of your entries are suddenly gone? A password has changed ?), it is wise to first come here and make a backup. You can then have a look inside the backup file to see if something is wrong (You might also want to create an issue [here](https://github.com/LedgerHQ/app-passwords/issues) so we fix your issue for all users). 35 | - If you want to add a lot of new passwords, the manual input on the device keyboard will show its limits. You can instead create a backup and edit it manually to add all your new entries. You just have to restore your app with this file and the job is done :) 36 | 37 | ## Building the project 38 | 39 | ```bash 40 | $ yarn install 41 | $ yarn start 42 | ``` 43 | 44 | Runs the app in the development mode.\ 45 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 46 | 47 | ## Credits 48 | 49 | This WebApp was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 50 | -------------------------------------------------------------------------------- /clients/wui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passwords-backup", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://passwords.ledger.com", 6 | "dependencies": { 7 | "@ledgerhq/hw-transport-webusb": "^5.49.0", 8 | "install": "^0.13.0", 9 | "react": "^17.0.1", 10 | "react-accessible-accordion": "^3.3.3", 11 | "react-dom": "^17.0.1", 12 | "react-markdown": "^5.0.2", 13 | "react-toastify": "^6.0.9", 14 | "web-vitals": "^0.2.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject", 21 | "predeploy": "npm run build", 22 | "deploy": "gh-pages -d build" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "@testing-library/jest-dom": "^5.11.4", 44 | "@testing-library/react": "^11.1.0", 45 | "@testing-library/user-event": "^12.1.10", 46 | "gh-pages": "^3.1.0", 47 | "prettier": "^2.1.2", 48 | "react-scripts": "4.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /clients/wui/public/CNAME: -------------------------------------------------------------------------------- 1 | passwords.ledger.com -------------------------------------------------------------------------------- /clients/wui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | Passwords Backup 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /clients/wui/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 40 | 42 | Created by potrace 1.16, written by Peter Selinger 2001-2019 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 59 | 63 | 67 | 71 | 75 | 79 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /clients/wui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/clients/wui/public/logo192.png -------------------------------------------------------------------------------- /clients/wui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/clients/wui/public/logo512.png -------------------------------------------------------------------------------- /clients/wui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Passwords Backup", 3 | "name": "Passwords Backup", 4 | "icons": [ 5 | { 6 | "src": "favicon.svg", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /clients/wui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /clients/wui/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | html {background-color: #232123} 6 | 7 | .App-banner { 8 | margin-top: 10vh; 9 | margin-bottom: 10vh; 10 | justify-content: center; 11 | display: flex; 12 | flex-direction: row; 13 | max-height: 10vh; 14 | color: white; 15 | } 16 | 17 | .App-header { 18 | min-width: 100vw; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | color: white; 24 | } 25 | 26 | .App-logo { 27 | width: 10vh; 28 | height: auto; 29 | pointer-events: none; 30 | } 31 | 32 | .App-title { 33 | margin-left: 1.5vh; 34 | font-size: 3vh; 35 | font-weight: 1; 36 | } 37 | 38 | .App-link { 39 | color: #61dafb; 40 | } 41 | 42 | .Commands { 43 | /* margin-top: 10vh; */ 44 | margin-bottom: 6vh; 45 | display: flex; 46 | flex-direction: row; 47 | 48 | } 49 | 50 | .Toastify__toast-container { 51 | width: 400px; 52 | } 53 | 54 | .App-footer a { 55 | color: #61dafb; 56 | text-decoration: none; 57 | } 58 | 59 | @media only screen and (orientation: landscape) { 60 | .App-footer { 61 | margin-top: 6vh; 62 | font-size: 1.2vh; 63 | } 64 | } 65 | 66 | @media only screen and (orientation: portrait) { 67 | .App-footer { 68 | margin-top: 6vw; 69 | font-size: 1.2vw; 70 | } 71 | } -------------------------------------------------------------------------------- /clients/wui/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import logo from "./logo.svg"; 3 | import RestoreButton from "./components/RestoreButton"; 4 | import BackupButton from "./components/BackupButton"; 5 | import PasswordsManager from "./controller/PasswordsManager.js"; 6 | import AppExplanations from "./components/AppExplanations"; 7 | import { ToastContainer, toast } from "react-toastify"; 8 | import "react-toastify/dist/ReactToastify.css"; 9 | import { listen } from "@ledgerhq/logs"; 10 | import packageJson from "../package.json"; 11 | import "./App.css"; 12 | 13 | const passwordsManager = new PasswordsManager(); 14 | listen((log) => { 15 | console.log(log); 16 | }); 17 | 18 | function App() { 19 | function ask_device(device_handler, request) { 20 | return new Promise(async (resolve) => { 21 | let result = null; 22 | try { 23 | if (!device_handler.connected) { 24 | await device_handler.connect(); 25 | setConnected(true); 26 | toast.info("Device connected 👌", { autoClose: false }); 27 | } 28 | if (request) { 29 | toast.info("Approve action on your device ✨", { autoClose: false }); 30 | setBusy(true); 31 | result = await request(); 32 | 33 | toast.dismiss(); 34 | toast.success("Success 🦄"); 35 | } 36 | } catch (error) { 37 | device_handler.disconnect(); 38 | setConnected(false); 39 | toast.dismiss(); 40 | toast.error(`${error.toString()} 🙅`); 41 | } finally { 42 | setBusy(false); 43 | resolve(result); 44 | } 45 | }); 46 | } 47 | 48 | // const = new PasswordsManager(true); 49 | 50 | const [isBusy, setBusy] = useState(false); 51 | const [isConnected, setConnected] = useState(false); 52 | 53 | return ( 54 |
55 | 56 |
57 | logo 58 |

Passwords Backup

59 |
60 |
61 |
62 |
92 | 93 |
94 |

95 | A modest Web App built at Ledger with React, hosted by Github. v 96 | {`${packageJson.version}`}.{" "} 97 | 98 | PRs welcomed and appreciated ✨ 99 | 100 |

101 |
102 |
103 |
104 | ); 105 | } 106 | 107 | export default App; 108 | -------------------------------------------------------------------------------- /clients/wui/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import App from "./App"; 3 | 4 | test("renders learn react link", () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /clients/wui/src/components/AppExplanations/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * ---------------------------------------------- 3 | * Demo styles 4 | * ---------------------------------------------- 5 | **/ 6 | 7 | .accordion { 8 | text-align: left; 9 | font: Monaco,Consolas,"Andale Mono","DejaVu Sans Mono",monospace; 10 | border: 5px solid rgba(0, 0, 0, 0.1); 11 | border-radius: 2px; 12 | } 13 | 14 | @media only screen and (orientation: landscape) { 15 | .accordion { 16 | width: 70vw; 17 | font-size: 1.2vw; 18 | } 19 | } 20 | 21 | @media only screen and (orientation: portrait) { 22 | .accordion { 23 | width: 90vw; 24 | font-size: 1.2vh; 25 | } 26 | } 27 | 28 | 29 | .accordion__item + .accordion__item { 30 | border-top: 5px solid rgba(0, 0, 0, 0.1); 31 | border-radius: 2px; 32 | } 33 | 34 | .accordion__button { 35 | background-color: #232123; 36 | color: #f4f4f4; 37 | cursor: pointer; 38 | padding: 18px; 39 | text-align: left; 40 | border: none; 41 | transition: 0.3s; 42 | } 43 | 44 | .accordion__button:hover { 45 | background-color: #444; 46 | } 47 | 48 | .accordion__button:before { 49 | display: inline-block; 50 | content: ''; 51 | height: 1vh; 52 | width: 1vh; 53 | margin-right: 12px; 54 | border-bottom: 2px solid currentColor; 55 | border-right: 2px solid currentColor; 56 | transform: rotate(-45deg); 57 | } 58 | 59 | @media only screen and (orientation: landscape) { 60 | .accordion__button:before { 61 | height: 1vw; 62 | width: 1vw; 63 | } 64 | } 65 | 66 | @media only screen and (orientation: portrait) { 67 | .accordion__button:before { 68 | height: 1vh; 69 | width: 1vh; 70 | } 71 | } 72 | 73 | .accordion__button[aria-expanded='true']::before, 74 | .accordion__button[aria-selected='true']::before { 75 | transform: rotate(45deg); 76 | } 77 | 78 | [hidden] { 79 | display: none; 80 | } 81 | 82 | .accordion__panel { 83 | padding: 20px; 84 | animation: fadein 0.35s ease-in; 85 | } 86 | 87 | code { 88 | color: #7CFC00; 89 | } 90 | 91 | a { 92 | color: #7CFC00; 93 | } 94 | 95 | /* -------------------------------------------------- */ 96 | /* ---------------- Animation part ------------------ */ 97 | /* -------------------------------------------------- */ 98 | 99 | @keyframes fadein { 100 | 0% { 101 | opacity: 0; 102 | } 103 | 104 | 100% { 105 | opacity: 1; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /clients/wui/src/components/AppExplanations/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | import { 4 | Accordion, 5 | AccordionItem, 6 | AccordionItemHeading, 7 | AccordionItemButton, 8 | AccordionItemPanel, 9 | } from "react-accessible-accordion"; 10 | import ReactMarkdown from "react-markdown"; 11 | import "./index.css"; 12 | 13 | export default function AppExplanations() { 14 | const [expandedItems, setexpandedItems] = useState([]); 15 | 16 | // In case the user expands a node that is barely visible, we scroll the page to display it fully 17 | function handleExpand(update) { 18 | if (update.length > expandedItems.length) { 19 | const newExpandedItemUUID = update[update.length - 1]; 20 | const itemButtonBottom = document 21 | .getElementById(`accordion__panel-${newExpandedItemUUID}`) 22 | .getBoundingClientRect().bottom; 23 | if (itemButtonBottom > window.innerHeight) { 24 | window.scrollBy(0, itemButtonBottom - window.innerHeight); 25 | } 26 | } 27 | setexpandedItems(update); 28 | } 29 | 30 | const whatIsThisWebApp_help = 31 | // eslint-disable-next-line 32 | "This Web App allows you to backup/restore the list of `password nicknames` stored inside the `Passwords app` on your Ledger Nano S/ Nano X. \n\ 33 | It is useful to have such a backup when you update the Passwords app on your device, or the device firmware, because the list gets erased. Another case where it's practical to have a nickname backup is when you loose your device: Restoring the [24-words recovery phrase](https://www.ledger.com/academy/crypto/what-is-a-recovery-phrase) is necessary but not sufficient to restore your passwords, you need your nickname list as well. \n\ 34 | The backup consists in a human readable `backup.json` file containing a dump of the 4096 bytes of application storage. \n\ 35 | Note that all operations of this Web App are done locally on your computer, there are no external communications occurring."; 36 | 37 | const whatIsTheLedgerPasswordsApp_help = 38 | "Look [here](https://github.com/LedgerHQ/app-passwords/blob/master/README.md) for more information on the device application itself."; 39 | 40 | const howToUseThisWebApp_help = 41 | // eslint-disable-next-line 42 | '* Connect your Nano S/X to your computer and open the `Passwords app`.\n* You can now click on the big `Connect` button, and if it succeeds the `Backup` and `Restore` buttons should replace the previous button. If you have troubles with this step, have a look [here](https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues). \n* Either click on `Backup` or `Restore` depending on what you want to do: \n\ 43 | * `Backup` will prompt a screen requesting your approval on your device (`"Transfer metadatas ?"`), then save a backup file. This is your backup. it\'s not confidential, so for instance you can send it to yourself by e-mail to never loose it. \n\ 44 | * `Restore` will prompt a file input dialog where you should indicate a previous backup file. A prompt (`"Overwrite metadatas ?"`) will then request your approval on your device. Done.'; 45 | 46 | const whichbrowsersAreSupported_help = 47 | "The communication with the device is done through `WebUSB`, which is currently supported only on `Google Chrome` / `Chromium` / `Brave` for `Linux` and `MacOS`. On `Windows`, you need to first go to `chrome://flags` then search for `Enable new USB backend`, disable it and relaunch Chrome."; 48 | 49 | const lessCommonUseCases_help = 50 | // eslint-disable-next-line 51 | "* If you ever encounter a WTF-kind of error with your passwords app (some or all of your entries are suddenly gone? A password has changed ?), it is wise to first come here and make a backup. You can then have a look inside the backup file to see if something is wrong (You might also want to create an issue [here](https://github.com/LedgerHQ/app-passwords/issues) so we fix your issue for all users). \n* If you want to add a lot of new passwords, the manual input on the device keyboard will show its limits. You can instead create a backup and edit it manually to add all your new entries. You just have to restore your app with this file and the job is done :)"; 52 | 53 | return ( 54 | 55 | 56 | 57 | What is this Web App ? 58 | 59 | 60 | 64 | 65 | 66 | 67 | 68 | 69 | What is the Ledger Passwords application ? 70 | 71 | 72 | 73 | 77 | 78 | 79 | 80 | 81 | How to use this Web App ? 82 | 83 | 84 | 88 | 89 | 90 | 91 | 92 | 93 | Which web browsers and operating systems are supported ? 94 | 95 | 96 | 97 | 101 | 102 | 103 | 104 | 105 | Less common use cases 106 | 107 | 108 | 112 | 113 | 114 | 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /clients/wui/src/components/BackupButton/index.css: -------------------------------------------------------------------------------- 1 | .BackupButton { 2 | border: none; 3 | color: white; 4 | border-radius: 12px; 5 | opacity: 1; 6 | transition: 0.3s; 7 | padding-left: 0; 8 | padding-right: 0; 9 | } 10 | 11 | @media only screen and (orientation: landscape) { 12 | .BackupButton { 13 | width: 15vw; 14 | height: 10vw; 15 | font-size: 3vw; 16 | } 17 | } 18 | 19 | @media only screen and (orientation: portrait) { 20 | .BackupButton { 21 | width: 15vh; 22 | height: 10vh; 23 | font-size: 3vh; 24 | } 25 | } 26 | 27 | .BackupButton:disabled {opacity: 0.5} 28 | 29 | .BackupButton:hover:enabled {opacity: 0.8} -------------------------------------------------------------------------------- /clients/wui/src/components/BackupButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react"; 2 | import "./index.css"; 3 | 4 | function downloadFile(fileData) { 5 | var blob = new Blob([JSON.stringify(fileData, null, 4)], { 6 | type: "application/json;charset=utf-8", 7 | }); 8 | var url = URL.createObjectURL(blob); 9 | var elem = document.createElement("a"); 10 | elem.href = url; 11 | elem.download = "backup.json"; 12 | document.body.appendChild(elem); 13 | elem.click(); 14 | document.body.removeChild(elem); 15 | } 16 | 17 | function BackupButton({ text, color, disabled, hidden, onClick }) { 18 | const [isLoading, setLoading] = useState(false); 19 | 20 | const onTriggerRunThenSaveFile = useCallback(() => { 21 | setLoading(true); 22 | onClick().then((fileData) => { 23 | if (fileData) downloadFile(fileData); 24 | setLoading(false); 25 | }); 26 | }, [onClick]); 27 | 28 | return ( 29 | 41 | ); 42 | } 43 | 44 | export default BackupButton; 45 | -------------------------------------------------------------------------------- /clients/wui/src/components/RestoreButton/index.css: -------------------------------------------------------------------------------- 1 | .RestoreButton { 2 | border: none; 3 | color: white; 4 | border-radius: 12px; 5 | opacity: 1; 6 | transition: 0.3s; 7 | padding-left: 0; 8 | padding-right: 0; 9 | } 10 | 11 | @media only screen and (orientation: landscape) { 12 | .RestoreButton { 13 | width: 15vw; 14 | height: 10vw; 15 | font-size: 3vw; 16 | } 17 | } 18 | 19 | @media only screen and (orientation: portrait) { 20 | .RestoreButton { 21 | width: 15vh; 22 | height: 10vh; 23 | font-size: 3vh; 24 | } 25 | } 26 | 27 | .RestoreButton:disabled {opacity: 0.5} 28 | 29 | .RestoreButton:hover:enabled {opacity: 0.8} -------------------------------------------------------------------------------- /clients/wui/src/components/RestoreButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useCallback, useState } from "react"; 2 | import "./index.css"; 3 | 4 | function RestoreButton({ text, color, disabled, hidden, onClick }) { 5 | const [isLoading, setLoading] = useState(false); 6 | const file = useRef(null); 7 | 8 | const hasFileInputBeenCanceled = () => { 9 | if (!file.current.value.length) setLoading(false); 10 | document.body.onfocus = null; 11 | }; 12 | 13 | const onTriggerFileSelect = useCallback(() => { 14 | setLoading(true); 15 | document.body.onfocus = hasFileInputBeenCanceled; 16 | file.current && file.current.click(); 17 | }, []); 18 | 19 | const onSelectedFileChanged = useCallback( 20 | (event) => { 21 | event.target.files[0].text().then((text) => { 22 | event.target.value = ""; 23 | onClick(text).then(() => setLoading(false)); 24 | }); 25 | }, 26 | [onClick] 27 | ); 28 | 29 | return ( 30 | 49 | ); 50 | } 51 | 52 | export default RestoreButton; 53 | -------------------------------------------------------------------------------- /clients/wui/src/controller/PasswordsManager.js: -------------------------------------------------------------------------------- 1 | import TransportWebUSB from "@ledgerhq/hw-transport-webusb"; 2 | 3 | const insAPDU = Object.freeze({ 4 | GET_APP_INFO_COMMAND: 0x01, 5 | GET_APP_CONFIG_COMMAND: 0x03, 6 | DUMP_METADATAS_COMMAND: 0x04, 7 | LOAD_METADATAS_COMMAND: 0x05, 8 | }); 9 | 10 | const passwordsCharsets = Object.freeze({ 11 | UPPERCASE: 1, 12 | LOWERCASE: 2, 13 | NUMBERS: 4, 14 | MINUS: 8, 15 | UNDERLINE: 16, 16 | SPACE: 32, 17 | SPECIAL: 64, 18 | BRACKETS: 128, 19 | }); 20 | 21 | const allPasswordsCharsets = 0xff; 22 | 23 | class PasswordsManager { 24 | constructor() { 25 | this.allowedStatuses = [ 26 | 0x9000, 27 | 0x6985, 28 | 0x6a86, 29 | 0x6a87, 30 | 0x6d00, 31 | 0x6e00, 32 | 0xb000, 33 | ]; 34 | this.connected = false; 35 | this.busy = false; 36 | this.transport = null; 37 | } 38 | 39 | async connect() { 40 | if (!this.connected) { 41 | if (!this.transport) this.transport = await TransportWebUSB.create(); 42 | try { 43 | const [appName, version] = await this.getAppInfo(); 44 | if (appName.toString() !== "Passwords") 45 | throw new Error("The Passwords app is not opened on the device"); 46 | this.version = version; 47 | let appConfig = await this.getAppConfig(); 48 | this.storage_size = appConfig["storage_size"]; 49 | this.connected = true; 50 | } catch (error) { 51 | await this.transport.close(); 52 | this.disconnect(); 53 | throw error; 54 | } 55 | } 56 | } 57 | 58 | isSuccess(result) { 59 | return ( 60 | result.length >= 2 && result.readUInt16BE(result.length - 2) === 0x9000 61 | ); 62 | } 63 | 64 | disconnect() { 65 | this.connected = false; 66 | this.transport = null; 67 | } 68 | 69 | mapProtocolError(result) { 70 | if (result.length < 2) throw new Error("Response length is too small"); 71 | 72 | var errors = { 73 | 0x6985: "Action cancelled", 74 | 0x6a86: "SW_WRONG_P1P2", 75 | 0x6a87: "SW_WRONG_DATA_LENGTH", 76 | 0x6d00: "SW_INS_NOT_SUPPORTED", 77 | 0x6e00: "SW_CLA_NOT_SUPPORTED", 78 | 0xb000: "SW_APPNAME_TOO_LONG", 79 | 0x6f10: "SW_METADATAS_PARSING_ERROR", 80 | }; 81 | 82 | let error = result.readUInt16BE(result.length - 2); 83 | if (error in errors) { 84 | throw new Error(errors[error]); 85 | } 86 | } 87 | 88 | _lock() { 89 | if (this.busy) throw new Error("Device is busy"); 90 | this.busy = true; 91 | } 92 | 93 | _unlock() { 94 | this.busy = false; 95 | } 96 | 97 | _charsetListToBitmask(charsets) { 98 | let bitmask = 0x00; 99 | for (const charset of charsets) { 100 | bitmask |= passwordsCharsets[charset]; 101 | } 102 | if (bitmask === 0x00) bitmask = allPasswordsCharsets; 103 | return bitmask; 104 | } 105 | 106 | _bitmaskToCharsetList(bitmask) { 107 | let charsetList = []; 108 | if (bitmask === 0x00 || bitmask === allPasswordsCharsets) { 109 | charsetList.push("ALL_SETS"); 110 | } else { 111 | for (const charset in passwordsCharsets) { 112 | if (passwordsCharsets[charset] & bitmask) charsetList.push(charset); 113 | } 114 | } 115 | return charsetList; 116 | } 117 | 118 | _toBytes(json_metadatas) { 119 | let metadatas = Buffer.alloc(this.storage_size); 120 | let parsed_metadatas = JSON.parse(json_metadatas)["parsed"]; 121 | let offset = 0; 122 | parsed_metadatas.forEach((element) => { 123 | let nickname = element["nickname"]; 124 | let charsets = this._charsetListToBitmask(element["charsets"]); 125 | if (nickname.length > 19) 126 | throw new Error( 127 | `Nickname too long (19 max): ${nickname} has length ${nickname.length}` 128 | ); 129 | if (offset + 3 + nickname.length >= this.storage_size) 130 | throw new Error( 131 | `Not enough memory on this device to restore this backup` 132 | ); 133 | metadatas[offset++] = nickname.length + 1; 134 | metadatas[offset++] = 0x00; 135 | metadatas[offset++] = charsets; 136 | metadatas.write(nickname, offset); 137 | offset += nickname.length; 138 | }); 139 | // mark free space at the end of the buffer 140 | metadatas[offset++] = 0x00; 141 | metadatas[offset++] = 0x00; 142 | return metadatas; 143 | } 144 | 145 | _toJSON(metadatas) { 146 | let metadatas_list = []; 147 | let erased_list = []; 148 | let offset = 0; 149 | let corruptions = []; 150 | while (true) { 151 | let len = metadatas[offset]; 152 | if (len === 0) break; 153 | let erased = metadatas[offset + 1] === 0xff ? true : false; 154 | let charsets = metadatas[offset + 2]; 155 | if (len > 19 + 1) 156 | corruptions += [offset, `nickname too long ${len}, max is 19`]; 157 | let metadata = { 158 | nickname: metadatas.slice(offset + 3, offset + 2 + len).toString(), 159 | charsets: this._bitmaskToCharsetList(charsets), 160 | }; 161 | erased ? erased_list.push(metadata) : metadatas_list.push(metadata); 162 | offset += len + 2; 163 | } 164 | return { 165 | parsed: metadatas_list, 166 | nicknames_erased_but_still_stored: erased_list, 167 | corruptions_encountered: corruptions, 168 | raw_metadatas: metadatas.toString("hex"), 169 | }; 170 | } 171 | 172 | async _load_metadatas_chunk(chunk, is_last) { 173 | let result = await this.transport.send( 174 | 0xe0, 175 | insAPDU.LOAD_METADATAS_COMMAND, 176 | is_last ? 0xff : 0x00, 177 | 0x00, 178 | Buffer.from(chunk), 179 | this.allowedStatuses 180 | ); 181 | if (!this.isSuccess(result)) this.mapProtocolError(result); 182 | return result; 183 | } 184 | async getAppInfo() { 185 | this._lock(); 186 | try { 187 | let result = await this.transport.send( 188 | 0xb0, 189 | insAPDU.GET_APP_INFO_COMMAND, 190 | 0x00, 191 | 0x00, 192 | Buffer(0), 193 | this.allowedStatuses 194 | ); 195 | if (!this.isSuccess(result)) this.mapProtocolError(result); 196 | 197 | result = result.slice(0, result.length - 2); 198 | let app_name, app_version; 199 | try { 200 | let offset = 1; 201 | let app_name_length = result[offset++]; 202 | app_name = result.slice(offset, offset + app_name_length).toString(); 203 | offset += app_name_length; 204 | let app_version_length = result[offset++]; 205 | app_version = result 206 | .slice(offset, offset + app_version_length) 207 | .toString(); 208 | return [app_name, app_version]; 209 | } catch (error) { 210 | throw new Error( 211 | `Unexpected result from device, parsing error: ${error}` 212 | ); 213 | } 214 | } finally { 215 | this._unlock(); 216 | } 217 | } 218 | 219 | async getAppConfig() { 220 | this._lock(); 221 | try { 222 | let result = await this.transport.send( 223 | 0xe0, 224 | insAPDU.GET_APP_CONFIG_COMMAND, 225 | 0x00, 226 | 0x00, 227 | Buffer(0), 228 | this.allowedStatuses 229 | ); 230 | if (!this.isSuccess(result)) this.mapProtocolError(result); 231 | result = result.slice(0, result.length - 2); 232 | if (result.length !== 6) 233 | throw new Error(`Can't parse app config of length ${result.length}`); 234 | 235 | let storage_size = result.readUInt32BE(0, 4); 236 | let keyboard_type = result[4]; 237 | let press_enter_after_typing = result[5]; 238 | return { storage_size, keyboard_type, press_enter_after_typing }; 239 | } finally { 240 | this._unlock(); 241 | } 242 | } 243 | 244 | async dump_metadatas() { 245 | this._lock(); 246 | try { 247 | let metadatas = Buffer.alloc(0); 248 | while (metadatas.length < this.storage_size) { 249 | let result = await this.transport.send( 250 | 0xe0, 251 | insAPDU.DUMP_METADATAS_COMMAND, 252 | 0x00, 253 | 0x00, 254 | Buffer(0), 255 | this.allowedStatuses 256 | ); 257 | if (!this.isSuccess(result)) this.mapProtocolError(result); 258 | metadatas = Buffer.concat([ 259 | metadatas, 260 | Buffer.from(result.slice(1, -2)), 261 | ]); 262 | if (result[0] === 0xff && metadatas.length < this.storage_size) { 263 | throw new Error( 264 | `${this.storage_size} bytes requested but only ${metadatas.length} bytes available` 265 | ); 266 | } 267 | } 268 | return this._toJSON(metadatas); 269 | } finally { 270 | this._unlock(); 271 | } 272 | } 273 | 274 | async load_metadatas(JSON_metadatas) { 275 | this._lock(); 276 | try { 277 | let metadatas = this._toBytes(JSON_metadatas); 278 | if (metadatas.length === 0) { 279 | throw new Error("No data to load"); 280 | } 281 | for (let i = 0; i < metadatas.length; i += 0xff) { 282 | let chunk = metadatas.slice(i, i + 0xff); 283 | await this._load_metadatas_chunk( 284 | chunk, 285 | i + chunk.length === metadatas.length ? true : false 286 | ); 287 | } 288 | } finally { 289 | this._unlock(); 290 | } 291 | } 292 | } 293 | 294 | export default PasswordsManager; 295 | -------------------------------------------------------------------------------- /clients/wui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: museo-sans,Open sans,arial,sans-serif; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | } 7 | 8 | code { 9 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 10 | monospace; 11 | } 12 | -------------------------------------------------------------------------------- /clients/wui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | -------------------------------------------------------------------------------- /clients/wui/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 40 | 42 | Created by potrace 1.16, written by Peter Selinger 2001-2019 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 59 | 63 | 67 | 71 | 75 | 79 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /clients/wui/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /glyphs/icon_back.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_back.gif -------------------------------------------------------------------------------- /glyphs/icon_backspace.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_backspace.gif -------------------------------------------------------------------------------- /glyphs/icon_backspace_invert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_backspace_invert.gif -------------------------------------------------------------------------------- /glyphs/icon_bootloader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_bootloader.gif -------------------------------------------------------------------------------- /glyphs/icon_certificate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_certificate.gif -------------------------------------------------------------------------------- /glyphs/icon_classes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_classes.gif -------------------------------------------------------------------------------- /glyphs/icon_classes_invert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_classes_invert.gif -------------------------------------------------------------------------------- /glyphs/icon_coggle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_coggle.gif -------------------------------------------------------------------------------- /glyphs/icon_crossmark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_crossmark.gif -------------------------------------------------------------------------------- /glyphs/icon_dashboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_dashboard.gif -------------------------------------------------------------------------------- /glyphs/icon_digits.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_digits.gif -------------------------------------------------------------------------------- /glyphs/icon_digits_invert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_digits_invert.gif -------------------------------------------------------------------------------- /glyphs/icon_down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_down.gif -------------------------------------------------------------------------------- /glyphs/icon_eye.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_eye.gif -------------------------------------------------------------------------------- /glyphs/icon_left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_left.gif -------------------------------------------------------------------------------- /glyphs/icon_lowercase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_lowercase.gif -------------------------------------------------------------------------------- /glyphs/icon_lowercase_invert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_lowercase_invert.gif -------------------------------------------------------------------------------- /glyphs/icon_plus.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_plus.gif -------------------------------------------------------------------------------- /glyphs/icon_right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_right.gif -------------------------------------------------------------------------------- /glyphs/icon_up.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_up.gif -------------------------------------------------------------------------------- /glyphs/icon_uppercase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_uppercase.gif -------------------------------------------------------------------------------- /glyphs/icon_uppercase_invert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_uppercase_invert.gif -------------------------------------------------------------------------------- /glyphs/icon_validate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_validate.gif -------------------------------------------------------------------------------- /glyphs/icon_validate_14.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_validate_14.gif -------------------------------------------------------------------------------- /glyphs/icon_validate_invert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_validate_invert.gif -------------------------------------------------------------------------------- /glyphs/icon_warning.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/icon_warning.gif -------------------------------------------------------------------------------- /glyphs/stax_icon_password_manager_64px.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/glyphs/stax_icon_password_manager_64px.gif -------------------------------------------------------------------------------- /icons/nanos_icon_password_manager.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/icons/nanos_icon_password_manager.gif -------------------------------------------------------------------------------- /icons/nanox_icon_password_manager.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/icons/nanox_icon_password_manager.gif -------------------------------------------------------------------------------- /icons/stax_icon_password_manager_32px.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/icons/stax_icon_password_manager_32px.gif -------------------------------------------------------------------------------- /include/hid_mapping.h: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Password Manager application 3 | * (c) 2017 Ledger 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ********************************************************************************/ 17 | 18 | #pragma once 19 | 20 | #define HID_MAPPING_H 21 | #define SHIFT_KEY 0x02 22 | #define ALT_KEY 0x04 23 | 24 | #include 25 | 26 | enum hid_mapping_e { 27 | HID_MAPPING_NONE = 0, 28 | HID_MAPPING_QWERTY = 1, 29 | HID_MAPPING_QWERTY_INTL = 2, 30 | HID_MAPPING_AZERTY = 3, 31 | }; 32 | typedef enum hid_mapping_e hid_mapping_t; 33 | 34 | void map_char(hid_mapping_t mapping, uint8_t key, uint8_t *out); 35 | -------------------------------------------------------------------------------- /include/password_generation.h: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Password Manager application 3 | * (c) 2017 Ledger 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ********************************************************************************/ 17 | 18 | #ifndef PASSWORD_GENERATION_H 19 | 20 | #define PASSWORD_GENERATION_H 21 | 22 | #include 23 | #include "ctr_drbg.h" 24 | 25 | typedef enum { 26 | UPPERCASE = 1, 27 | LOWERCASE = 2, 28 | NUMBERS = 4, 29 | MINUS = 8, 30 | UNDERLINE = 16, 31 | SPACE = 32, 32 | SPECIAL = 64, 33 | BRACKETS = 128, 34 | 35 | ALL_SETS = 0xFF, 36 | } setmask_t; 37 | 38 | #define NUM_SETS 8 39 | 40 | uint32_t generate_password(mbedtls_ctr_drbg_context *drbg, 41 | setmask_t setMask, 42 | const uint8_t *minFromSet, 43 | uint8_t *out, 44 | uint32_t size); 45 | 46 | #endif 47 | -------------------------------------------------------------------------------- /include/usbd_hid_impl.h: -------------------------------------------------------------------------------- 1 | #ifndef USBD_HID_IMPL_H 2 | #define USBD_HID_IMPL_H 3 | 4 | #define HID_EPIN_ADDR 0x82 5 | #define HID_EPIN_SIZE 0x40 6 | 7 | #define HID_EPOUT_ADDR 0x02 8 | #define HID_EPOUT_SIZE 0x40 9 | 10 | #endif // USBD_HID_IMPL_H 11 | -------------------------------------------------------------------------------- /ledger_app.toml: -------------------------------------------------------------------------------- 1 | [app] 2 | build_directory="." 3 | devices = ["nanos", "nanos+", "nanox", "stax"] 4 | sdk = "c" 5 | 6 | [tests] 7 | unit_directory = "./tests/unit" 8 | pytest_directory = "./tests/functional" 9 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | requires_phyical_device: mark a test that should run only with a real device -------------------------------------------------------------------------------- /src/apdu_handlers/dump_metadatas.c: -------------------------------------------------------------------------------- 1 | #include "error.h" 2 | #include "globals.h" 3 | #include "handlers.h" 4 | #include "io.h" 5 | #include "ui.h" 6 | 7 | int dump_metadatas() { 8 | if (app_state.user_approval == false) { 9 | app_state.bytes_transferred = 0; 10 | message_pair_t msg = {"Transfer", "metadatas ?"}; 11 | ui_request_user_approval(&msg); 12 | return 0; 13 | } 14 | 15 | size_t remaining_bytes_count = sizeof(N_storage.metadatas) - app_state.bytes_transferred; 16 | size_t payload_size; 17 | 18 | if (remaining_bytes_count < MAX_PAYLOAD_SIZE) { 19 | app_state.user_approval = false; 20 | payload_size = remaining_bytes_count; 21 | G_io_apdu_buffer[TRANSFER_FLAG_OFFSET] = LAST_CHUNK; 22 | ui_idle(); 23 | } else { 24 | payload_size = MAX_PAYLOAD_SIZE; 25 | G_io_apdu_buffer[TRANSFER_FLAG_OFFSET] = MORE_DATA_INCOMING; 26 | } 27 | 28 | memcpy(&G_io_apdu_buffer[TRANSFER_PAYLOAD_OFFSET], 29 | (const void*) N_storage.metadatas + app_state.bytes_transferred, 30 | payload_size); 31 | 32 | app_state.bytes_transferred += payload_size; 33 | 34 | return io_send_response_pointer(G_io_apdu_buffer, 35 | payload_size + TRANSFER_PAYLOAD_OFFSET, 36 | SW_OK); 37 | } 38 | -------------------------------------------------------------------------------- /src/apdu_handlers/get_app_config.c: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Password Manager application 3 | * (c) 2017-2023 Ledger SAS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ********************************************************************************/ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "error.h" 25 | #include "globals.h" 26 | #include "handlers.h" 27 | #include "types.h" 28 | #include "ui.h" 29 | 30 | int get_app_config(uint8_t p1, uint8_t p2, __attribute__((unused)) const buf_t* input) { 31 | if (p1 != 0 || p2 != 0) { 32 | return io_send_sw(SW_WRONG_P1P2); 33 | } 34 | 35 | uint8_t* config = G_io_apdu_buffer; 36 | size_t offset = 0; 37 | 38 | config[offset++] = ((size_t) MAX_METADATAS) >> 8 * 3; 39 | config[offset++] = ((size_t) MAX_METADATAS) >> 8 * 2; 40 | config[offset++] = ((size_t) MAX_METADATAS) >> 8 * 1; 41 | config[offset++] = ((size_t) MAX_METADATAS) & 0xFF; 42 | 43 | config[offset++] = N_storage.keyboard_layout; 44 | config[offset++] = N_storage.press_enter_after_typing; 45 | 46 | ui_idle(); 47 | return io_send_response_pointer(config, offset, SW_OK); 48 | } 49 | -------------------------------------------------------------------------------- /src/apdu_handlers/handlers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "types.h" 6 | 7 | #define TRANSFER_FLAG_OFFSET 0 8 | #define TRANSFER_PAYLOAD_OFFSET 1 9 | 10 | /* max payload size = 260 - sizeof(flag) - sizeof(status_word) */ 11 | #define MAX_PAYLOAD_SIZE (IO_APDU_BUFFER_SIZE - TRANSFER_PAYLOAD_OFFSET - 2) 12 | 13 | #define MORE_DATA_INCOMING 0x00 14 | #define LAST_CHUNK 0xFF 15 | 16 | int dump_metadatas(); 17 | int get_app_config(uint8_t p1, uint8_t p2, const buf_t *input); 18 | int load_metadatas(uint8_t p1, uint8_t p2, const buf_t *input); 19 | -------------------------------------------------------------------------------- /src/apdu_handlers/load_metadatas.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "error.h" 4 | #include "globals.h" 5 | #include "handlers.h" 6 | #include "metadata.h" 7 | #include "ui.h" 8 | 9 | int load_metadatas(uint8_t p1, uint8_t p2, const buf_t *input) { 10 | if ((p1 != 0 && p1 != LAST_CHUNK) || p2 != 0) { 11 | return io_send_sw(SW_WRONG_P1P2); 12 | } 13 | if (app_state.user_approval == false) { 14 | app_state.bytes_transferred = 0; 15 | message_pair_t msg = {"Overwrite", "metadatas ?"}; 16 | ui_request_user_approval(&msg); 17 | return 0; 18 | } 19 | 20 | if (input->size > sizeof(N_storage.metadatas) - app_state.bytes_transferred) { 21 | return io_send_sw(SW_WRONG_DATA_LENGTH); 22 | } 23 | 24 | override_metadatas(app_state.bytes_transferred, (void *) input->bytes, input->size); 25 | app_state.bytes_transferred += input->size; 26 | 27 | if (app_state.bytes_transferred >= sizeof(N_storage.metadatas) || p1 == LAST_CHUNK) { 28 | // reset state 29 | app_state.user_approval = false; 30 | ui_idle(); 31 | if (compact_metadata()) { 32 | return io_send_sw(SW_METADATAS_PARSING_ERROR); 33 | } 34 | } 35 | 36 | return io_send_sw(SW_OK); 37 | } 38 | -------------------------------------------------------------------------------- /src/app_main.c: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Password Manager application 3 | * (c) 2017-2023 Ledger SAS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ********************************************************************************/ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | #include "glyphs.h" 31 | #include "dispatcher.h" 32 | #include "error.h" 33 | #include "globals.h" 34 | #include "metadata.h" 35 | #include "password_typing.h" 36 | #include "ui.h" 37 | 38 | const internalStorage_t N_storage_real; 39 | app_state_t app_state; 40 | volatile unsigned int G_led_status; 41 | 42 | void app_main() { 43 | int input_len = 0; 44 | 45 | init_storage(); 46 | memset(&app_state, 0, sizeof(app_state)); 47 | 48 | ui_idle(); 49 | 50 | io_init(); 51 | app_state.output_len = 0; 52 | 53 | #if defined(POPULATE) 54 | #include "password.h" 55 | // removing 1 as `sizeof` will include the trailing null byte in the result (10) 56 | // but this app stores password without this trailing null byte. 57 | create_new_password("password1", sizeof("password1") - 1); 58 | create_new_password("password2", sizeof("password2") - 1); 59 | create_new_password("password3", sizeof("password3") - 1); 60 | #endif 61 | for (;;) { 62 | BEGIN_TRY { 63 | TRY { 64 | input_len = io_recv_command(); 65 | if (input_len == -1) { 66 | return; 67 | } 68 | PRINTF("=> %.*H\n", input_len, G_io_apdu_buffer); 69 | if (input_len < OFFSET_CDATA || 70 | input_len - OFFSET_CDATA != G_io_apdu_buffer[OFFSET_LC]) { 71 | io_send_sw(SW_WRONG_DATA_LENGTH); 72 | continue; 73 | } 74 | if (dispatch() < 0) { 75 | return; 76 | } 77 | } 78 | CATCH(EXCEPTION_IO_RESET) { 79 | THROW(EXCEPTION_IO_RESET); 80 | } 81 | CATCH_OTHER(e) { 82 | io_send_sw(e); 83 | } 84 | FINALLY { 85 | } 86 | END_TRY; 87 | } 88 | } 89 | } 90 | 91 | void app_exit(void) { 92 | BEGIN_TRY_L(exit) { 93 | TRY_L(exit) { 94 | os_sched_exit(-1); 95 | } 96 | FINALLY_L(exit) { 97 | } 98 | } 99 | END_TRY_L(exit); 100 | } 101 | -------------------------------------------------------------------------------- /src/ctaes/COPYING: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pieter Wuille 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ctaes/README.md: -------------------------------------------------------------------------------- 1 | ctaes 2 | ===== 3 | 4 | Simple C module for constant-time AES encryption and decryption. 5 | 6 | Features: 7 | * Simple, pure C code without any dependencies. 8 | * No tables or data-dependent branches whatsoever, but using bit sliced approach from https://eprint.iacr.org/2009/129.pdf. 9 | * Very small object code: slightly over 4k of executable code when compiled with -Os. 10 | * Slower than implementations based on precomputed tables or specialized instructions, but can do ~15 MB/s on modern CPUs. 11 | 12 | Performance 13 | ----------- 14 | 15 | Compiled with GCC 5.3.1 with -O3, on an Intel(R) Core(TM) i7-4800MQ CPU, numbers in CPU cycles: 16 | 17 | | Algorithm | Key schedule | Encryption per byte | Decryption per byte | 18 | | --------- | ------------:| -------------------:| -------------------:| 19 | | AES-128 | 2.8k | 154 | 161 | 20 | | AES-192 | 3.1k | 169 | 181 | 21 | | AES-256 | 4.0k | 191 | 203 | 22 | 23 | Build steps 24 | ----------- 25 | 26 | Object code: 27 | 28 | $ gcc -O3 ctaes.c -c -o ctaes.o 29 | 30 | Tests: 31 | 32 | $ gcc -O3 ctaes.c test.c -o test 33 | 34 | Benchmark: 35 | 36 | $ gcc -O3 ctaes.c bench.c -o bench 37 | 38 | Review 39 | ------ 40 | 41 | Results of a formal review of the code can be found in http://bitcoin.sipa.be/ctaes/review.zip 42 | -------------------------------------------------------------------------------- /src/ctaes/ctaes.h: -------------------------------------------------------------------------------- 1 | /********************************************************************* 2 | * Copyright (c) 2016 Pieter Wuille * 3 | * Distributed under the MIT software license, see the accompanying * 4 | * file COPYING or http://www.opensource.org/licenses/mit-license.php.* 5 | **********************************************************************/ 6 | 7 | #ifndef _CTAES_H_ 8 | #define _CTAES_H_ 1 9 | 10 | #include 11 | #include 12 | 13 | typedef struct { 14 | uint16_t slice[8]; 15 | } AES_state; 16 | 17 | typedef struct { 18 | AES_state rk[11]; 19 | } AES128_ctx; 20 | 21 | typedef struct { 22 | AES_state rk[13]; 23 | } AES192_ctx; 24 | 25 | typedef struct { 26 | AES_state rk[15]; 27 | } AES256_ctx; 28 | 29 | void AES128_init(AES128_ctx* ctx, const unsigned char* key16); 30 | void AES128_encrypt(const AES128_ctx* ctx, 31 | size_t blocks, 32 | unsigned char* cipher16, 33 | const unsigned char* plain16); 34 | void AES128_decrypt(const AES128_ctx* ctx, 35 | size_t blocks, 36 | unsigned char* plain16, 37 | const unsigned char* cipher16); 38 | 39 | void AES192_init(AES192_ctx* ctx, const unsigned char* key24); 40 | void AES192_encrypt(const AES192_ctx* ctx, 41 | size_t blocks, 42 | unsigned char* cipher16, 43 | const unsigned char* plain16); 44 | void AES192_decrypt(const AES192_ctx* ctx, 45 | size_t blocks, 46 | unsigned char* plain16, 47 | const unsigned char* cipher16); 48 | 49 | void AES256_init(AES256_ctx* ctx, const unsigned char* key32); 50 | void AES256_encrypt(const AES256_ctx* ctx, 51 | size_t blocks, 52 | unsigned char* cipher16, 53 | const unsigned char* plain16); 54 | void AES256_decrypt(const AES256_ctx* ctx, 55 | size_t blocks, 56 | unsigned char* plain16, 57 | const unsigned char* cipher16); 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /src/dispatcher.c: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Password Manager application 3 | * (c) 2017-2023 Ledger SAS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ********************************************************************************/ 17 | 18 | #include 19 | #include 20 | #include 21 | 22 | #include "apdu_handlers/handlers.h" 23 | #include "dispatcher.h" 24 | #include "error.h" 25 | #include "globals.h" 26 | #include "tests/tests.h" 27 | #include "types.h" 28 | 29 | int dispatch() { 30 | if (G_io_apdu_buffer[OFFSET_CLA] != CLA) { 31 | return io_send_sw(SW_CLA_NOT_SUPPORTED); 32 | } 33 | 34 | uint8_t ins = G_io_apdu_buffer[OFFSET_INS]; 35 | if (app_state.current_command != ins) { 36 | app_state.current_command = ins; 37 | app_state.user_approval = false; 38 | } 39 | 40 | const buf_t input = {.bytes = G_io_apdu_buffer + OFFSET_CDATA, 41 | .size = G_io_apdu_buffer[OFFSET_LC]}; 42 | uint8_t p1 = G_io_apdu_buffer[OFFSET_P1]; 43 | uint8_t p2 = G_io_apdu_buffer[OFFSET_P2]; 44 | 45 | switch (ins) { 46 | case GET_APP_CONFIG: 47 | return get_app_config(p1, p2, &input); 48 | case DUMP_METADATAS: 49 | return dump_metadatas(); 50 | case LOAD_METADATAS: 51 | return load_metadatas(p1, p2, &input); 52 | 53 | #ifdef TESTING 54 | case RUN_TEST: 55 | return test_dispatcher(p1, p2, &input); 56 | #endif 57 | default: 58 | return io_send_sw(SW_INS_NOT_SUPPORTED); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/dispatcher.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "types.h" 6 | 7 | int dispatch(); 8 | -------------------------------------------------------------------------------- /src/error.c: -------------------------------------------------------------------------------- 1 | #include "error.h" 2 | 3 | static const message_pair_t ERR_MESSAGES[5] = { 4 | // OK 5 | {}, 6 | // ERR_NO_MORE_SPACE_AVAILABLE 7 | {"Write Error", "Database is full"}, 8 | // ERR_CORRUPTED_METADATA 9 | {"Write Error", "Database should be repaired, please contact Ledger Support"}, 10 | // ERR_NO_METADATA 11 | {"Erase Error", "Database already empty"}, 12 | // ERR_METADATA_ENTRY_TOO_BIG 13 | {"Write Error", "Entry is too big"}}; 14 | 15 | message_pair_t get_error(const error_type_t error) { 16 | return ERR_MESSAGES[error]; 17 | } 18 | -------------------------------------------------------------------------------- /src/error.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "types.h" 4 | 5 | #define SW_OK 0x9000 6 | #define SW_CONDITIONS_OF_USE_NOT_SATISFIED 0x6985 7 | #define SW_WRONG_P1P2 0x6A86 8 | #define SW_WRONG_DATA_LENGTH 0x6A87 9 | #define SW_INS_NOT_SUPPORTED 0x6D00 10 | #define SW_CLA_NOT_SUPPORTED 0x6E00 11 | #define SW_APPNAME_TOO_LONG 0xB000 12 | #define SW_METADATAS_PARSING_ERROR 0x6F10 13 | 14 | typedef enum error_type_e { 15 | OK = 0, 16 | ERR_NO_MORE_SPACE_AVAILABLE = 1, 17 | ERR_CORRUPTED_METADATA = 2, 18 | ERR_NO_METADATA = 3, 19 | ERR_METADATA_ENTRY_TOO_BIG = 4 20 | } error_type_t; 21 | 22 | message_pair_t get_error(const error_type_t error); 23 | -------------------------------------------------------------------------------- /src/globals.c: -------------------------------------------------------------------------------- 1 | #include "globals.h" 2 | #include "options.h" 3 | 4 | void init_storage() { 5 | if (N_storage.magic == STORAGE_MAGIC) { 6 | // already initialized 7 | return; 8 | } 9 | uint32_t tmp = STORAGE_MAGIC; 10 | nvm_write((void *) &N_storage.magic, (void *) &tmp, sizeof(uint32_t)); 11 | tmp = 0; 12 | nvm_write((void *) &N_storage.press_enter_after_typing, 13 | (void *) &tmp, 14 | sizeof(N_storage.press_enter_after_typing)); 15 | nvm_write((void *) &N_storage.keyboard_layout, 16 | (void *) &tmp, 17 | sizeof(N_storage.keyboard_layout)); 18 | nvm_write((void *) &N_storage.metadata_count, (void *) &tmp, sizeof(N_storage.metadata_count)); 19 | nvm_write((void *) N_storage.metadatas, (void *) &tmp, 2); 20 | init_charset_options(); 21 | } 22 | -------------------------------------------------------------------------------- /src/globals.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "types.h" 6 | 7 | extern const internalStorage_t N_storage_real; 8 | extern app_state_t app_state; 9 | extern volatile unsigned int G_led_status; 10 | 11 | #define CLA 0xE0 12 | #define N_storage (*(volatile internalStorage_t*) PIC(&N_storage_real)) 13 | 14 | void init_storage(void); 15 | -------------------------------------------------------------------------------- /src/hid_mapping.c: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Password Manager application 3 | * (c) 2017-2023 Ledger SAS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ********************************************************************************/ 17 | 18 | #include 19 | #include 20 | 21 | #define KEYCODE_START 0x20 22 | #define MOD_MASK_LENGTH 12 23 | #define MOD2_MASK_LENGTH 24 24 | #define MAPPING_LENGTH 95 25 | 26 | static const uint8_t TWOPOWER[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; 27 | 28 | typedef struct mapping_s { 29 | uint8_t qwerty; 30 | uint8_t azerty; 31 | } mapping_t; 32 | 33 | static const mapping_t MAP[] = { 34 | // alt mask from ascii 0x20 35 | {0x00, 0x08}, 36 | {0x00, 0x00}, 37 | {0x00, 0x00}, 38 | {0x00, 0x00}, 39 | {0x00, 0x01}, 40 | {0x00, 0x00}, 41 | {0x00, 0x00}, 42 | {0x00, 0x78}, 43 | {0x00, 0x01}, 44 | {0x00, 0x00}, 45 | {0x00, 0x00}, 46 | {0x00, 0x78}, 47 | // shift mask from ascii 0x20 48 | {0x7E, 0x20}, 49 | {0x0f, 0xc8}, 50 | {0x00, 0xff}, 51 | {0xd4, 0xc3}, 52 | {0xff, 0xfe}, 53 | {0xff, 0xff}, 54 | {0xff, 0xff}, 55 | {0xc7, 0x07}, 56 | {0x00, 0x00}, 57 | {0x00, 0x00}, 58 | {0x00, 0x00}, 59 | {0x78, 0x00}, 60 | // key codes from ascii 0x20 61 | {0x2c, 0x2c}, // ' ' (i = 32 - 8) 62 | {0x1e, 0x38}, // '!' 63 | {0x34, 0x20}, // '"' 64 | {0x20, 0x20}, // '#' 65 | {0x21, 0x30}, // '$' 66 | {0x22, 0x34}, // '%' 67 | {0x24, 0x1e}, // '&' 68 | {0x34, 0x21}, // ''' 69 | {0x26, 0x22}, // '(' 70 | {0x27, 0x2d}, // ')' 71 | {0x25, 0x32}, // '*' 72 | {0x2e, 0x2e}, // '+' 73 | {0x36, 0x10}, // ',' 74 | {0x2d, 0x23}, // '-' 75 | {0x37, 0x36}, // '.' 76 | {0x38, 0x37}, // '/' 77 | {0x27, 0x27}, // '0' (i = 48 - 8) 78 | {0x1e, 0x1e}, // '1' 79 | {0x1f, 0x1f}, 80 | {0x20, 0x20}, 81 | {0x21, 0x21}, 82 | {0x22, 0x22}, 83 | {0x23, 0x23}, 84 | {0x24, 0x24}, 85 | {0x25, 0x25}, 86 | {0x26, 0x26}, // '9' 87 | {0x33, 0x37}, // ':' 88 | {0x33, 0x36}, // ';' 89 | {0x36, 0x64}, // '<' 90 | {0x2e, 0x2e}, // '=' 91 | {0x37, 0x64}, // '>' 92 | {0x38, 0x10}, // '?' 93 | {0x1f, 0x27}, // '@' 94 | {0x04, 0x14}, // 'A' (i = 65 - 8) 95 | {0x05, 0x05}, 96 | {0x06, 0x06}, 97 | {0x07, 0x07}, 98 | {0x08, 0x08}, 99 | {0x09, 0x09}, 100 | {0x0a, 0x0a}, 101 | {0x0b, 0x0b}, 102 | {0x0c, 0x0c}, 103 | {0x0d, 0x0d}, 104 | {0x0e, 0x0e}, 105 | {0x0f, 0x0f}, 106 | {0x10, 0x33}, 107 | {0x11, 0x11}, 108 | {0x12, 0x12}, 109 | {0x13, 0x13}, 110 | {0x14, 0x04}, 111 | {0x15, 0x15}, 112 | {0x16, 0x16}, 113 | {0x17, 0x17}, 114 | {0x18, 0x18}, 115 | {0x19, 0x19}, 116 | {0x1a, 0x1d}, 117 | {0x1b, 0x1b}, 118 | {0x1c, 0x1c}, 119 | {0x1d, 0x1a}, // 'Z' (i = 90 - 8) 120 | {0x2f, 0x22}, // '[' 121 | {0x31, 0x25}, // '\' 122 | {0x30, 0x2D}, // ']' 123 | {0x23, 0x26}, // '^' 124 | {0x2d, 0x25}, // '_' 125 | {0x35, 0x24}, // '`' 126 | {0x04, 0x14}, // 'a' (i = 97 - 8) 127 | {0x05, 0x05}, 128 | {0x06, 0x06}, 129 | {0x07, 0x07}, 130 | {0x08, 0x08}, 131 | {0x09, 0x09}, 132 | {0x0a, 0x0a}, 133 | {0x0b, 0x0b}, 134 | {0x0c, 0x0c}, 135 | {0x0d, 0x0d}, 136 | {0x0e, 0x0e}, 137 | {0x0f, 0x0f}, 138 | {0x10, 0x33}, 139 | {0x11, 0x11}, 140 | {0x12, 0x12}, 141 | {0x13, 0x13}, 142 | {0x14, 0x04}, 143 | {0x15, 0x15}, 144 | {0x16, 0x16}, 145 | {0x17, 0x17}, 146 | {0x18, 0x18}, 147 | {0x19, 0x19}, 148 | {0x1a, 0x1d}, 149 | {0x1b, 0x1b}, 150 | {0x1c, 0x1c}, 151 | {0x1d, 0x1a}, // 'z' (i = 122 - 8) 152 | {0x2f, 0x21}, // '{' 153 | {0x31, 0x23}, // '|' 154 | {0x30, 0x2e}, // '}' 155 | {0x35, 0x1f}, // '~' (i = 126 - 8) 156 | }; 157 | 158 | #if 0 159 | // a good test string 160 | out = "a&b~c#d {e\"f'g(h -i _j)k=l+m [n |o \\p^q @r ]s }t$u!v:w/x;y.z,A?B D`EFGHIJKLMNOPQRSTUVWXYZ0123456789"; 161 | #endif 162 | 163 | static uint8_t get_char(uint8_t index, hid_mapping_t mapping) { 164 | // all mapping != AZERTY fall back to QWERTY + some adjustments 165 | return (mapping == HID_MAPPING_AZERTY ? MAP[index].azerty : MAP[index].qwerty); 166 | } 167 | 168 | void map_char(hid_mapping_t mapping, uint8_t key, uint8_t *out) { 169 | uint8_t keyDiv8, twoPower, keyCode; 170 | bool altUsed, shiftUsed; 171 | 172 | if (key < KEYCODE_START) { 173 | THROW(EXCEPTION); 174 | } 175 | key -= KEYCODE_START; 176 | if (key > MAPPING_LENGTH) { 177 | THROW(EXCEPTION); 178 | } 179 | keyDiv8 = (key / 8); 180 | twoPower = TWOPOWER[key % 8]; 181 | altUsed = ((get_char(keyDiv8, mapping) & twoPower) != 0); 182 | shiftUsed = ((get_char(MOD_MASK_LENGTH + keyDiv8, mapping) & twoPower) != 0); 183 | keyCode = get_char(MOD2_MASK_LENGTH + key, mapping); 184 | out[0] = (altUsed ? ALT_KEY : 0x00) | (shiftUsed ? SHIFT_KEY : 0x00); 185 | out[1] = 0x00; 186 | out[2] = keyCode; 187 | } 188 | -------------------------------------------------------------------------------- /src/keyboards/bolos_ux_nanos_keyboard.c: -------------------------------------------------------------------------------- 1 | 2 | #include "os.h" 3 | #include "cx.h" 4 | #include "os_io_seproxyhal.h" 5 | #include "string.h" 6 | #include "glyphs.h" 7 | 8 | #include "keyboard.h" 9 | 10 | #if defined(TARGET_NANOS) 11 | 12 | const bagl_element_t screen_common_keyboard_elements[] = { 13 | 14 | // erase 15 | {{BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, 16 | 17 | // typed word 18 | {{BAGL_RECTANGLE, 0x00, 18, 18, 110 - 18, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, 19 | {{BAGL_LABELINE, 20 | 0x10, 21 | 128 / 2 - 12 / 2 - 40, 22 | 28, 23 | 12, 24 | 12, 25 | 0, 26 | 0, 27 | 0, 28 | 0x000000, 29 | 0xFFFFFF, 30 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 31 | 0}, 32 | NULL}, 33 | {{BAGL_LABELINE, 34 | 0x11, 35 | 128 / 2 - 12 / 2 - 30, 36 | 28, 37 | 12, 38 | 12, 39 | 0, 40 | 0, 41 | 0, 42 | 0x000000, 43 | 0xFFFFFF, 44 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 45 | 0}, 46 | NULL}, 47 | {{BAGL_LABELINE, 48 | 0x12, 49 | 128 / 2 - 12 / 2 - 20, 50 | 28, 51 | 12, 52 | 12, 53 | 0, 54 | 0, 55 | 0, 56 | 0x000000, 57 | 0xFFFFFF, 58 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 59 | 0}, 60 | NULL}, 61 | {{BAGL_LABELINE, 62 | 0x13, 63 | 128 / 2 - 12 / 2 - 10, 64 | 28, 65 | 12, 66 | 12, 67 | 0, 68 | 0, 69 | 0, 70 | 0x000000, 71 | 0xFFFFFF, 72 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 73 | 0}, 74 | NULL}, 75 | {{BAGL_LABELINE, 76 | 0x14, 77 | 128 / 2 - 12 / 2, 78 | 28, 79 | 12, 80 | 12, 81 | 0, 82 | 0, 83 | 0, 84 | 0x000000, 85 | 0xFFFFFF, 86 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 87 | 0}, 88 | NULL}, 89 | {{BAGL_LABELINE, 90 | 0x15, 91 | 128 / 2 - 12 / 2 + 10, 92 | 28, 93 | 12, 94 | 12, 95 | 0, 96 | 0, 97 | 0, 98 | 0x000000, 99 | 0xFFFFFF, 100 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 101 | 0}, 102 | NULL}, 103 | {{BAGL_LABELINE, 104 | 0x16, 105 | 128 / 2 - 12 / 2 + 20, 106 | 28, 107 | 12, 108 | 12, 109 | 0, 110 | 0, 111 | 0, 112 | 0x000000, 113 | 0xFFFFFF, 114 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 115 | 0}, 116 | NULL}, 117 | {{BAGL_LABELINE, 118 | 0x17, 119 | 128 / 2 - 12 / 2 + 30, 120 | 28, 121 | 12, 122 | 12, 123 | 0, 124 | 0, 125 | 0, 126 | 0x000000, 127 | 0xFFFFFF, 128 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 129 | 0}, 130 | NULL}, 131 | {{BAGL_LABELINE, 132 | 0x18, 133 | 128 / 2 - 12 / 2 + 40, 134 | 28, 135 | 12, 136 | 12, 137 | 0, 138 | 0, 139 | 0, 140 | 0x000000, 141 | 0xFFFFFF, 142 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 143 | 0}, 144 | NULL}, 145 | 146 | // slider elements 147 | {{BAGL_LINE, 0x06, 46, 8, 3, 1, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, NULL}, 148 | {{BAGL_LINE, 0x07, 79, 8, 3, 1, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, NULL}, 149 | 150 | // previous element 151 | {{BAGL_LABELINE, 152 | 0x01, 153 | 26, 154 | 12, 155 | 14, 156 | 13, 157 | 0, 158 | 0, 159 | BAGL_FILL, 160 | 0xFFFFFF, 161 | 0x000000, 162 | BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, 163 | 0}, 164 | NULL}, 165 | 166 | // current item 167 | {{BAGL_RECTANGLE, 0x22, 57, 12 - 10, 14, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, 168 | {{BAGL_LABELINE, 169 | 0x02, 170 | 58, 171 | 12, 172 | 12, 173 | 13, 174 | 0, 175 | 0, 176 | BAGL_FILL, 177 | 0x000000, 178 | 0xFFFFFF, 179 | BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, 180 | 0}, 181 | NULL}, 182 | //{{BAGL_LABELINE , 0x02, 57, 12, 14, 13, 0, 4, BAGL_FILL, 0x000000, 183 | // 0xFFFFFF, 184 | // BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER|BAGL_FONT_ALIGNMENT_MIDDLE, 0 185 | // }, 186 | // NULL }, 187 | 188 | // next element 189 | {{BAGL_LABELINE, 190 | 0x03, 191 | 88, 192 | 12, 193 | 14, 194 | 13, 195 | 0, 196 | 0, 197 | BAGL_FILL, 198 | 0xFFFFFF, 199 | 0x000000, 200 | BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, 201 | 0}, 202 | NULL}, 203 | 204 | // left/rights icons 205 | {{BAGL_ICON, 0x00, 3, 12, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, (const char*) &C_icon_left}, 206 | {{BAGL_ICON, 0x00, 121, 12, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, 207 | (const char*) &C_icon_right}, 208 | }; 209 | 210 | const bagl_element_t* screen_common_keyboard_before_element_display_callback( 211 | const bagl_element_t* element) { 212 | const bagl_element_t* e; 213 | // copy element to be displayed 214 | memmove(&G_ux.tmp_element, (void*) PIC(element), sizeof(G_ux.tmp_element)); 215 | 216 | switch (element->component.userid) { 217 | case 0x01: 218 | if (G_keyboard_ctx.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { 219 | return 0; 220 | } 221 | return G_keyboard_ctx.keyboard_callback(KEYBOARD_RENDER_ITEM, 222 | G_keyboard_ctx.hslider3_before); 223 | 224 | // current item (both line and invert rectangle) 225 | case 0x22: 226 | case 0x02: 227 | e = G_keyboard_ctx.keyboard_callback(KEYBOARD_RENDER_ITEM, 228 | G_keyboard_ctx.hslider3_current); 229 | 230 | // if the current component to display is not TEXT, then don't display the invert 231 | // rectangle, to avoid graphic glitch 232 | if (element->component.userid == 0x22) { 233 | if (e->component.type == BAGL_ICON) { 234 | return NULL; 235 | } 236 | } 237 | return e; 238 | break; 239 | 240 | case 0x03: 241 | if (G_keyboard_ctx.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { 242 | return 0; 243 | } 244 | return G_keyboard_ctx.keyboard_callback(KEYBOARD_RENDER_ITEM, 245 | G_keyboard_ctx.hslider3_after); 246 | 247 | case 0x06: 248 | if (G_keyboard_ctx.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { 249 | return 0; // don't display 250 | } 251 | break; 252 | 253 | case 0x07: 254 | if (G_keyboard_ctx.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { 255 | return 0; // don't display 256 | } 257 | break; 258 | 259 | default: 260 | if (element->component.userid & 0x10) { 261 | // request the xieth word char 262 | return G_keyboard_ctx.keyboard_callback(KEYBOARD_RENDER_WORD, 263 | element->component.userid & 0x0F); 264 | } 265 | break; 266 | } 267 | // return the probably modded element by the callback function 268 | return &G_ux.tmp_element; 269 | } 270 | 271 | unsigned int screen_common_keyboard_button(unsigned int button_mask, 272 | unsigned int button_mask_counter 273 | __attribute__((unused))) { 274 | switch (button_mask) { 275 | case BUTTON_EVT_RELEASED | BUTTON_LEFT | BUTTON_RIGHT: // validate current digit 276 | 277 | // validate the item, and if accepted, then redisplay current screen, else don't draw 278 | // anything 279 | if (G_keyboard_ctx.keyboard_callback(KEYBOARD_ITEM_VALIDATED, 280 | G_keyboard_ctx.hslider3_current)) { 281 | goto redraw; 282 | } 283 | break; 284 | 285 | case BUTTON_EVT_FAST | BUTTON_LEFT: 286 | case BUTTON_EVT_RELEASED | BUTTON_LEFT: 287 | bolos_ux_hslider3_previous(); 288 | goto redraw; 289 | 290 | case BUTTON_EVT_FAST | BUTTON_RIGHT: 291 | case BUTTON_EVT_RELEASED | BUTTON_RIGHT: 292 | bolos_ux_hslider3_next(); 293 | 294 | redraw: 295 | ux_stack_display(G_ux.stack_count - 1); 296 | break; 297 | } 298 | return 1; 299 | } 300 | 301 | void screen_common_keyboard_init(unsigned int stack_slot, 302 | unsigned int current_element, 303 | unsigned int nb_elements, 304 | keyboard_callback_t callback) { 305 | unsigned int current = G_keyboard_ctx.hslider3_current; 306 | ux_stack_init(stack_slot); 307 | 308 | // initiate the rotating modulo iterator 309 | bolos_ux_hslider3_init(nb_elements); 310 | if (current_element == COMMON_KEYBOARD_INDEX_UNCHANGED) { 311 | bolos_ux_hslider3_set_current(current); 312 | } else { 313 | bolos_ux_hslider3_set_current(current_element); 314 | } 315 | 316 | G_ux.stack[stack_slot].element_arrays[0].element_array = screen_common_keyboard_elements; 317 | G_ux.stack[stack_slot].element_arrays[0].element_array_count = 318 | ARRAYLEN(screen_common_keyboard_elements); 319 | G_ux.stack[stack_slot].element_arrays_count = 1; 320 | G_ux.stack[stack_slot].screen_before_element_display_callback = 321 | screen_common_keyboard_before_element_display_callback; // used for each screen of the 322 | // validate pin flow 323 | G_ux.stack[stack_slot].button_push_callback = screen_common_keyboard_button; 324 | G_keyboard_ctx.keyboard_callback = callback; 325 | 326 | ux_stack_display(stack_slot); 327 | } 328 | 329 | #endif 330 | -------------------------------------------------------------------------------- /src/keyboards/bolos_ux_nanox_keyboard.c: -------------------------------------------------------------------------------- 1 | #include "os.h" 2 | #include "cx.h" 3 | 4 | #include "os_io_seproxyhal.h" 5 | #include "string.h" 6 | 7 | #include "keyboard.h" 8 | 9 | #include "glyphs.h" 10 | 11 | #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) 12 | 13 | const bagl_element_t screen_common_keyboard_elements[] = { 14 | 15 | // erase 16 | {{BAGL_RECTANGLE, 0x00, 0, 0, 128, 64, 0, 0, BAGL_FILL, 0x000000, 0xFFFFFF, 0, 0}, NULL}, 17 | 18 | // title 19 | {{BAGL_LABELINE, 20 | 0x04, 21 | 0, 22 | 20, 23 | 128, 24 | 32 - 5, 25 | 0, 26 | 0, 27 | 0, 28 | 0xFFFFFF, 29 | 0x000000, 30 | BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, 31 | 0}, 32 | NULL}, 33 | 34 | // typed word 35 | {{BAGL_LABELINE, 36 | 0x10, 37 | 128 / 2 - 12 / 2 - 40, 38 | 48 + 5, 39 | 14, 40 | 14, 41 | 0, 42 | 0, 43 | 0, 44 | 0xFFFFFF, 45 | 0x000000, 46 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 47 | 0}, 48 | NULL}, 49 | {{BAGL_LABELINE, 50 | 0x11, 51 | 128 / 2 - 12 / 2 - 30, 52 | 48 + 5, 53 | 14, 54 | 14, 55 | 0, 56 | 0, 57 | 0, 58 | 0xFFFFFF, 59 | 0x000000, 60 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 61 | 0}, 62 | NULL}, 63 | {{BAGL_LABELINE, 64 | 0x12, 65 | 128 / 2 - 12 / 2 - 20, 66 | 48 + 5, 67 | 14, 68 | 14, 69 | 0, 70 | 0, 71 | 0, 72 | 0xFFFFFF, 73 | 0x000000, 74 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 75 | 0}, 76 | NULL}, 77 | {{BAGL_LABELINE, 78 | 0x13, 79 | 128 / 2 - 12 / 2 - 10, 80 | 48 + 5, 81 | 14, 82 | 14, 83 | 0, 84 | 0, 85 | 0, 86 | 0xFFFFFF, 87 | 0x000000, 88 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 89 | 0}, 90 | NULL}, 91 | {{BAGL_LABELINE, 92 | 0x14, 93 | 128 / 2 - 12 / 2, 94 | 48 + 5, 95 | 14, 96 | 14, 97 | 0, 98 | 0, 99 | 0, 100 | 0xFFFFFF, 101 | 0x000000, 102 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 103 | 0}, 104 | NULL}, 105 | {{BAGL_LABELINE, 106 | 0x15, 107 | 128 / 2 - 12 / 2 + 10, 108 | 48 + 5, 109 | 14, 110 | 14, 111 | 0, 112 | 0, 113 | 0, 114 | 0xFFFFFF, 115 | 0x000000, 116 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 117 | 0}, 118 | NULL}, 119 | {{BAGL_LABELINE, 120 | 0x16, 121 | 128 / 2 - 12 / 2 + 20, 122 | 48 + 5, 123 | 14, 124 | 14, 125 | 0, 126 | 0, 127 | 0, 128 | 0xFFFFFF, 129 | 0x000000, 130 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 131 | 0}, 132 | NULL}, 133 | {{BAGL_LABELINE, 134 | 0x17, 135 | 128 / 2 - 12 / 2 + 30, 136 | 48 + 5, 137 | 14, 138 | 14, 139 | 0, 140 | 0, 141 | 0, 142 | 0xFFFFFF, 143 | 0x000000, 144 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 145 | 0}, 146 | NULL}, 147 | {{BAGL_LABELINE, 148 | 0x18, 149 | 128 / 2 - 12 / 2 + 40, 150 | 48 + 5, 151 | 14, 152 | 14, 153 | 0, 154 | 0, 155 | 0, 156 | 0xFFFFFF, 157 | 0x000000, 158 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 159 | 0}, 160 | NULL}, 161 | 162 | // slider elements 163 | {{BAGL_LABELINE, 164 | 0x01, 165 | 29, 166 | 36, 167 | 14, 168 | 13, 169 | 0, 170 | 0, 171 | BAGL_FILL, 172 | 0xFFFFFF, 173 | 0x000000, 174 | BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, 175 | 0}, 176 | NULL}, 177 | {{BAGL_LINE, 0x06, 48, 32, 4, 1, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, NULL}, 178 | {{BAGL_RECTANGLE, 0x00, 57, 36 - 10, 14, 14, 0, 4, BAGL_FILL, 0xFFFFFF, 0x000000, 0, 0}, NULL}, 179 | {{BAGL_LABELINE, 180 | 0x02, 181 | 58, 182 | 36, 183 | 12, 184 | 13, 185 | 0, 186 | 0, 187 | BAGL_FILL, 188 | 0x000000, 189 | 0xFFFFFF, 190 | BAGL_FONT_OPEN_SANS_EXTRABOLD_11px | BAGL_FONT_ALIGNMENT_CENTER, 191 | 0}, 192 | NULL}, 193 | {{BAGL_LINE, 0x07, 76, 32, 4, 1, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, NULL}, 194 | {{BAGL_LABELINE, 195 | 0x03, 196 | 85, 197 | 36, 198 | 14, 199 | 13, 200 | 0, 201 | 0, 202 | BAGL_FILL, 203 | 0xFFFFFF, 204 | 0x000000, 205 | BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, 206 | 0}, 207 | NULL}, 208 | 209 | // left/rights icons 210 | {{BAGL_ICON, 0x0A, 2, 28, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, (char*) &C_icon_left}, 211 | {{BAGL_ICON, 0x0B, 122, 28, 4, 7, 0, 0, 0, 0xFFFFFF, 0x000000, 0, 0}, (char*) &C_icon_right}, 212 | }; 213 | 214 | const bagl_element_t* screen_common_keyboard_before_element_display_callback( 215 | const bagl_element_t* element) { 216 | // copy element to be displayed 217 | memmove(&G_ux.tmp_element, (void*) PIC(element), sizeof(G_ux.tmp_element)); 218 | 219 | switch (element->component.userid) { 220 | case 0x01: 221 | if (G_keyboard_ctx.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { 222 | return 0; 223 | } 224 | return G_keyboard_ctx.keyboard_callback(KEYBOARD_RENDER_ITEM, 225 | G_keyboard_ctx.hslider3_before); 226 | 227 | case 0x02: 228 | return G_keyboard_ctx.keyboard_callback(KEYBOARD_RENDER_ITEM, 229 | G_keyboard_ctx.hslider3_current); 230 | 231 | case 0x03: 232 | if (G_keyboard_ctx.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { 233 | return 0; 234 | } 235 | return G_keyboard_ctx.keyboard_callback(KEYBOARD_RENDER_ITEM, 236 | G_keyboard_ctx.hslider3_after); 237 | 238 | case 0x04: 239 | // display the title 240 | G_ux.tmp_element.text = G_keyboard_ctx.title; 241 | break; 242 | 243 | case 0x06: 244 | if (G_keyboard_ctx.hslider3_before == BOLOS_UX_HSLIDER3_NONE) { 245 | return 0; // don't display 246 | } 247 | break; 248 | 249 | case 0x07: 250 | if (G_keyboard_ctx.hslider3_after == BOLOS_UX_HSLIDER3_NONE) { 251 | return 0; // don't display 252 | } 253 | break; 254 | 255 | default: 256 | if (element->component.userid & 0x10) { 257 | // request the xieth word char 258 | return G_keyboard_ctx.keyboard_callback(KEYBOARD_RENDER_WORD, 259 | element->component.userid & 0x0F); 260 | } 261 | break; 262 | } 263 | // return the probably modded element by the callback function 264 | return &G_ux.tmp_element; 265 | } 266 | 267 | unsigned int screen_common_keyboard_button(unsigned int button_mask, 268 | unsigned int button_mask_counter 269 | __attribute__((unused))) { 270 | switch (button_mask) { 271 | case BUTTON_EVT_RELEASED | BUTTON_LEFT | BUTTON_RIGHT: // validate current digit 272 | 273 | // validate the item, and if accepted, then redisplay current screen, else don't draw 274 | // anything 275 | if (G_keyboard_ctx.keyboard_callback(KEYBOARD_ITEM_VALIDATED, 276 | G_keyboard_ctx.hslider3_current)) { 277 | goto redraw; 278 | } 279 | break; 280 | 281 | case BUTTON_EVT_FAST | BUTTON_LEFT: 282 | case BUTTON_EVT_RELEASED | BUTTON_LEFT: 283 | bolos_ux_hslider3_previous(); 284 | goto redraw; 285 | 286 | case BUTTON_EVT_FAST | BUTTON_RIGHT: 287 | case BUTTON_EVT_RELEASED | BUTTON_RIGHT: 288 | bolos_ux_hslider3_next(); 289 | 290 | redraw: 291 | ux_stack_display(G_ux.stack_count - 1); 292 | break; 293 | } 294 | return 1; 295 | } 296 | 297 | void screen_common_keyboard_init(unsigned int stack_slot, 298 | unsigned int current_element, 299 | unsigned int nb_elements, 300 | keyboard_callback_t callback) { 301 | unsigned int current = G_keyboard_ctx.hslider3_current; 302 | ux_stack_init(stack_slot); 303 | 304 | // initiate the rotating modulo iterator 305 | bolos_ux_hslider3_init(nb_elements); 306 | if (current_element == COMMON_KEYBOARD_INDEX_UNCHANGED) { 307 | bolos_ux_hslider3_set_current(current); 308 | } else { 309 | bolos_ux_hslider3_set_current(current_element); 310 | } 311 | 312 | G_ux.stack[stack_slot].element_arrays[0].element_array = screen_common_keyboard_elements; 313 | G_ux.stack[stack_slot].element_arrays[0].element_array_count = 314 | ARRAYLEN(screen_common_keyboard_elements); 315 | G_ux.stack[stack_slot].element_arrays_count = 1; 316 | G_ux.stack[stack_slot].screen_before_element_display_callback = 317 | screen_common_keyboard_before_element_display_callback; // used for each screen of the 318 | // validate pin flow 319 | G_ux.stack[stack_slot].button_push_callback = screen_common_keyboard_button; 320 | G_keyboard_ctx.keyboard_callback = callback; 321 | 322 | ux_stack_display(stack_slot); 323 | } 324 | 325 | #endif 326 | -------------------------------------------------------------------------------- /src/keyboards/keyboard.h: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Password Manager application 3 | * (c) 2017-2023 Ledger SAS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ********************************************************************************/ 17 | 18 | #pragma once 19 | 20 | #if !defined(TARGET_STAX) 21 | 22 | #include 23 | #include 24 | 25 | #define KEYBOARD_ITEM_VALIDATED \ 26 | 1 // callback is called with the entered item index, tmp_element is 27 | // precharged with element to be displayed and using the common string 28 | // buffer as string parameter 29 | #define KEYBOARD_RENDER_ITEM \ 30 | 2 // callback is called the element index, tmp_element is precharged with 31 | // element to be displayed and using the common string buffer as string 32 | // parameter 33 | #define KEYBOARD_RENDER_WORD \ 34 | 3 // callback is called with a -1 when requesting complete word, or the char 35 | // index else, returning 0 implies no char is to be displayed 36 | typedef const bagl_element_t* (*keyboard_callback_t)(unsigned int event, unsigned int value); 37 | 38 | // bolos ux context (not mandatory if redesigning a bolos ux) 39 | typedef struct keyboard_ctx { 40 | bagl_element_t tmp_element; 41 | 42 | unsigned int onboarding_step; 43 | 44 | unsigned int words_buffer_length; 45 | // after an int to make sure it's aligned 46 | char string_buffer[10]; 47 | 48 | char words_buffer[25]; 49 | 50 | #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) 51 | char title[20]; 52 | #endif 53 | 54 | // slider management 55 | unsigned int hslider3_before; 56 | unsigned int hslider3_current; 57 | unsigned int hslider3_after; 58 | unsigned int hslider3_total; 59 | 60 | keyboard_callback_t keyboard_callback; 61 | 62 | } keyboard_ctx_t; 63 | 64 | extern keyboard_ctx_t G_keyboard_ctx; 65 | 66 | // update before, current, after index for horizontal slider with 3 positions 67 | // slider distinguish handling from the data, to be more generic :) 68 | #define BOLOS_UX_HSLIDER3_NONE (-1UL) 69 | void bolos_ux_hslider3_init(unsigned int total_count); 70 | void bolos_ux_hslider3_set_current(unsigned int current); 71 | void bolos_ux_hslider3_next(void); 72 | void bolos_ux_hslider3_previous(void); 73 | 74 | #define COMMON_KEYBOARD_INDEX_UNCHANGED (-1UL) 75 | 76 | void screen_common_keyboard_init(unsigned int stack_slot, 77 | unsigned int current_element, 78 | unsigned int nb_elements, 79 | keyboard_callback_t callback); 80 | void screen_text_keyboard_init(char* buffer, unsigned int maxsize, appmain_t validation_callback); 81 | 82 | #endif 83 | -------------------------------------------------------------------------------- /src/keyboards/keyboard_common.c: -------------------------------------------------------------------------------- 1 | #include "os.h" 2 | #include "cx.h" 3 | #include "os_io_seproxyhal.h" 4 | #include "string.h" 5 | 6 | #include "keyboard.h" 7 | 8 | #if !defined(TARGET_STAX) 9 | 10 | void bolos_ux_hslider3_init(unsigned int total_count) { 11 | G_keyboard_ctx.hslider3_total = total_count; 12 | switch (total_count) { 13 | case 0: 14 | G_keyboard_ctx.hslider3_before = BOLOS_UX_HSLIDER3_NONE; 15 | G_keyboard_ctx.hslider3_current = BOLOS_UX_HSLIDER3_NONE; 16 | G_keyboard_ctx.hslider3_after = BOLOS_UX_HSLIDER3_NONE; 17 | break; 18 | case 1: 19 | G_keyboard_ctx.hslider3_before = BOLOS_UX_HSLIDER3_NONE; 20 | G_keyboard_ctx.hslider3_current = 0; 21 | G_keyboard_ctx.hslider3_after = BOLOS_UX_HSLIDER3_NONE; 22 | break; 23 | case 2: 24 | G_keyboard_ctx.hslider3_before = BOLOS_UX_HSLIDER3_NONE; 25 | // G_keyboard_ctx.hslider3_before = 1; // full rotate 26 | G_keyboard_ctx.hslider3_current = 0; 27 | G_keyboard_ctx.hslider3_after = 1; 28 | break; 29 | default: 30 | G_keyboard_ctx.hslider3_before = total_count - 1; 31 | G_keyboard_ctx.hslider3_current = 0; 32 | G_keyboard_ctx.hslider3_after = 1; 33 | break; 34 | } 35 | } 36 | 37 | void bolos_ux_hslider3_set_current(unsigned int current) { 38 | // index is reachable ? 39 | if (G_keyboard_ctx.hslider3_total > current) { 40 | // reach it 41 | while (G_keyboard_ctx.hslider3_current != current) { 42 | bolos_ux_hslider3_next(); 43 | } 44 | } 45 | } 46 | 47 | void bolos_ux_hslider3_next(void) { 48 | switch (G_keyboard_ctx.hslider3_total) { 49 | case 0: 50 | case 1: 51 | break; 52 | case 2: 53 | switch (G_keyboard_ctx.hslider3_current) { 54 | case 0: 55 | G_keyboard_ctx.hslider3_before = 0; 56 | G_keyboard_ctx.hslider3_current = 1; 57 | G_keyboard_ctx.hslider3_after = BOLOS_UX_HSLIDER3_NONE; 58 | break; 59 | case 1: 60 | G_keyboard_ctx.hslider3_before = BOLOS_UX_HSLIDER3_NONE; 61 | G_keyboard_ctx.hslider3_current = 0; 62 | G_keyboard_ctx.hslider3_after = 1; 63 | break; 64 | } 65 | break; 66 | default: 67 | G_keyboard_ctx.hslider3_before = G_keyboard_ctx.hslider3_current; 68 | G_keyboard_ctx.hslider3_current = G_keyboard_ctx.hslider3_after; 69 | G_keyboard_ctx.hslider3_after = 70 | (G_keyboard_ctx.hslider3_after + 1) % G_keyboard_ctx.hslider3_total; 71 | break; 72 | } 73 | } 74 | 75 | void bolos_ux_hslider3_previous(void) { 76 | switch (G_keyboard_ctx.hslider3_total) { 77 | case 0: 78 | case 1: 79 | break; 80 | case 2: 81 | switch (G_keyboard_ctx.hslider3_current) { 82 | case 0: 83 | G_keyboard_ctx.hslider3_before = 0; 84 | G_keyboard_ctx.hslider3_current = 1; 85 | G_keyboard_ctx.hslider3_after = BOLOS_UX_HSLIDER3_NONE; 86 | break; 87 | case 1: 88 | G_keyboard_ctx.hslider3_before = BOLOS_UX_HSLIDER3_NONE; 89 | G_keyboard_ctx.hslider3_current = 0; 90 | G_keyboard_ctx.hslider3_after = 1; 91 | break; 92 | } 93 | break; 94 | default: 95 | G_keyboard_ctx.hslider3_after = G_keyboard_ctx.hslider3_current; 96 | G_keyboard_ctx.hslider3_current = G_keyboard_ctx.hslider3_before; 97 | G_keyboard_ctx.hslider3_before = 98 | (G_keyboard_ctx.hslider3_before + G_keyboard_ctx.hslider3_total - 1) % 99 | G_keyboard_ctx.hslider3_total; 100 | break; 101 | } 102 | } 103 | 104 | #endif 105 | -------------------------------------------------------------------------------- /src/keyboards/text_keyboard.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #if !defined(TARGET_STAX) 7 | 8 | #include "keyboard.h" 9 | 10 | const char* const screen_keyboard_classes_elements[] = { 11 | // when first letter is already entered 12 | "abcdefghijklmnopqrstuvwxyz\b\n\r", 13 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ\b\n\r", 14 | "0123456789 '\"`&/?!:;.,~*$=+-[](){}<>\\_#@|%\b\n\r", 15 | // when first letter is not entered yet 16 | "abcdefghijklmnopqrstuvwxyz\r", 17 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ\r", 18 | "0123456789 '\"`&/?!:;.,~*$=+-[](){}<>\\_#@|%\r", 19 | }; 20 | 21 | #define GET_CHAR(char_class, char_idx) \ 22 | ((char*) PIC(screen_keyboard_classes_elements[char_class]))[char_idx] 23 | 24 | // these icons will be centered 25 | const bagl_icon_details_t* const screen_keyboard_classes_icons[] = { 26 | &C_icon_lowercase, 27 | &C_icon_uppercase, 28 | &C_icon_digits, 29 | &C_icon_backspace, 30 | &C_icon_validate, 31 | &C_icon_classes, 32 | #if defined(TARGET_NANOX) || defined(TARGET_NANOS2) 33 | &C_icon_lowercase_invert, 34 | &C_icon_uppercase_invert, 35 | &C_icon_digits_invert, 36 | &C_icon_backspace_invert, 37 | &C_icon_validate_invert, 38 | &C_icon_classes_invert, 39 | #endif 40 | }; 41 | 42 | const bagl_element_t* screen_keyboard_class_callback(unsigned int event, unsigned int value); 43 | appmain_t screen_keyboard_validation; 44 | char* screen_keyboard_buffer; 45 | unsigned int screen_keyboard_buffer_maxsize; 46 | 47 | #define PP_BUFFER screen_keyboard_buffer 48 | 49 | void screen_keyboard_render_icon(unsigned int value) { 50 | const bagl_icon_details_t* icon; 51 | #if defined(TARGET_NANOS) 52 | icon = (bagl_icon_details_t*) PIC(screen_keyboard_classes_icons[value]); 53 | G_ux.tmp_element.component.y = 5; 54 | #elif defined(TARGET_NANOX) || defined(TARGET_NANOS2) 55 | uint8_t inverted = G_ux.tmp_element.component.userid == 0x02; 56 | icon = (bagl_icon_details_t*) PIC(screen_keyboard_classes_icons[value + (inverted ? 6 : 0)]); 57 | G_ux.tmp_element.component.y -= 7; 58 | #endif 59 | G_ux.tmp_element.component.x += G_ux.tmp_element.component.width / 2 - icon->width / 2; 60 | G_ux.tmp_element.component.width = icon->width; 61 | G_ux.tmp_element.component.height = icon->height; 62 | G_ux.tmp_element.component.type = BAGL_ICON; 63 | G_ux.tmp_element.component.icon_id = 0; 64 | G_ux.tmp_element.text = (const char*) icon; 65 | } 66 | 67 | const bagl_element_t* screen_keyboard_item_callback(unsigned int event, unsigned int value) { 68 | switch (event) { 69 | case KEYBOARD_ITEM_VALIDATED: 70 | // depending on the chosen class, interpret the click 71 | if (GET_CHAR(G_keyboard_ctx.onboarding_step, value) == '\b') { 72 | if (strlen(PP_BUFFER)) { 73 | PP_BUFFER[strlen(PP_BUFFER) - 1] = 0; 74 | goto redisplay_current_class; 75 | } 76 | } else if (GET_CHAR(G_keyboard_ctx.onboarding_step, value) == '\r') { 77 | // go back to classes display 78 | screen_common_keyboard_init(0, 79 | G_keyboard_ctx.onboarding_step, 80 | (strlen(PP_BUFFER) ? 5 : 3), 81 | screen_keyboard_class_callback); 82 | return NULL; 83 | } else if (GET_CHAR(G_keyboard_ctx.onboarding_step, value) == '\n') { 84 | screen_keyboard_validation(); 85 | return NULL; 86 | } else { 87 | // too much character entered already 88 | if (strlen(PP_BUFFER) >= 89 | screen_keyboard_buffer_maxsize /*sizeof(G_keyboard_ctx.words_buffer) - 10*/ 90 | /*max pin + pin length*/) { 91 | // validate entry. for the user to continue entering if needed (chunking) 92 | screen_keyboard_validation(); 93 | return NULL; 94 | } 95 | // append the char and display classes again 96 | PP_BUFFER[strlen(PP_BUFFER)] = GET_CHAR(G_keyboard_ctx.onboarding_step, value); 97 | PP_BUFFER[strlen(PP_BUFFER)] = 0; 98 | 99 | redisplay_current_class: 100 | // redisplay the correct class depending on the current number of entered digits 101 | G_keyboard_ctx.onboarding_step = 102 | (G_keyboard_ctx.onboarding_step % 3) + (strlen(PP_BUFFER) ? 0 : 3); 103 | screen_common_keyboard_init( 104 | 0, 105 | (event == KEYBOARD_ITEM_VALIDATED && strlen(PP_BUFFER) == 0) 106 | ? 0 107 | : COMMON_KEYBOARD_INDEX_UNCHANGED, 108 | strlen((char*) PIC( 109 | screen_keyboard_classes_elements[G_keyboard_ctx.onboarding_step])), 110 | screen_keyboard_item_callback); 111 | return NULL; 112 | } 113 | break; 114 | 115 | case KEYBOARD_RENDER_ITEM: 116 | G_ux.tmp_element.text = G_ux.string_buffer; 117 | memset(G_ux.string_buffer, 0, 3); 118 | if (GET_CHAR(G_keyboard_ctx.onboarding_step, value) == '\b') { 119 | value = 3; 120 | } else if (GET_CHAR(G_keyboard_ctx.onboarding_step, value) == '\r') { 121 | value = 5; 122 | } else if (GET_CHAR(G_keyboard_ctx.onboarding_step, value) == '\n') { 123 | value = 4; 124 | } else { 125 | G_ux.string_buffer[0] = GET_CHAR(G_keyboard_ctx.onboarding_step, value); 126 | break; 127 | } 128 | screen_keyboard_render_icon(value); 129 | break; 130 | 131 | case KEYBOARD_RENDER_WORD: { 132 | unsigned int l = strlen(PP_BUFFER); 133 | 134 | G_ux.string_buffer[0] = '_'; 135 | G_ux.string_buffer[1] = 0; 136 | 137 | if (value < 8) { 138 | if (l <= 8) { 139 | if (l > value) { 140 | G_ux.string_buffer[0] = PP_BUFFER[value]; 141 | } else { 142 | G_ux.string_buffer[0] = '_'; 143 | } 144 | } else { 145 | // first char is '...' to notify continuing 146 | if (value == 0) { 147 | G_ux.string_buffer[0] = '.'; 148 | G_ux.string_buffer[1] = '.'; 149 | G_ux.string_buffer[2] = '.'; 150 | G_ux.string_buffer[3] = 0; 151 | } else { 152 | G_ux.string_buffer[0] = (PP_BUFFER + l - 8)[value]; 153 | } 154 | } 155 | } 156 | // ensure font is left aligned 157 | G_ux.tmp_element.text = G_ux.string_buffer; 158 | break; 159 | } 160 | } 161 | // update element display 162 | return &G_ux.tmp_element; 163 | } 164 | 165 | const bagl_element_t* screen_keyboard_class_callback(unsigned int event, unsigned int value) { 166 | switch (event) { 167 | case KEYBOARD_ITEM_VALIDATED: 168 | switch (value) { 169 | case 3: 170 | // backspace 171 | if (strlen(PP_BUFFER)) { 172 | PP_BUFFER[strlen(PP_BUFFER) - 1] = 0; 173 | screen_common_keyboard_init( 174 | 0, 175 | strlen(PP_BUFFER) == 0 ? 0 : COMMON_KEYBOARD_INDEX_UNCHANGED, 176 | (strlen(PP_BUFFER) ? 5 : 3), 177 | screen_keyboard_class_callback); 178 | return NULL; 179 | } 180 | break; 181 | case 4: 182 | screen_keyboard_validation(); 183 | return NULL; 184 | 185 | case 0: 186 | /* fallthrough */ 187 | __attribute__((fallthrough)); 188 | case 1: 189 | /* fallthrough */ 190 | __attribute__((fallthrough)); 191 | case 2: 192 | G_keyboard_ctx.onboarding_step = value + (strlen(PP_BUFFER) ? 0 : 3); 193 | screen_common_keyboard_init( 194 | 0, 195 | 0, 196 | strlen(&GET_CHAR(G_keyboard_ctx.onboarding_step, 0)), 197 | screen_keyboard_item_callback); 198 | return NULL; 199 | 200 | default: 201 | // no validation 202 | break; 203 | } 204 | break; 205 | case KEYBOARD_RENDER_ITEM: 206 | screen_keyboard_render_icon(value); 207 | break; 208 | case KEYBOARD_RENDER_WORD: 209 | // same as when drawing items 210 | return screen_keyboard_item_callback(event, value); 211 | } 212 | // update element display 213 | return &G_ux.tmp_element; 214 | } 215 | 216 | void screen_text_keyboard_init(char* buffer, unsigned int maxsize, appmain_t validation_callback) { 217 | screen_keyboard_buffer = buffer; 218 | screen_keyboard_buffer_maxsize = maxsize; 219 | screen_keyboard_validation = validation_callback; 220 | screen_common_keyboard_init(0, 0, 3, screen_keyboard_class_callback); 221 | } 222 | 223 | #endif 224 | -------------------------------------------------------------------------------- /src/metadata.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "metadata.h" 4 | #include "globals.h" 5 | 6 | error_type_t write_metadata(uint8_t *data, uint8_t dataSize) { 7 | if (dataSize > MAX_METANAME) { 8 | dataSize = MAX_METANAME; 9 | } 10 | error_type_t err = compact_metadata(); 11 | if (err) { 12 | return err; 13 | } 14 | uint32_t offset = find_free_metadata(); 15 | if ((offset + dataSize + 2 + 2) > MAX_METADATAS) { 16 | return ERR_NO_MORE_SPACE_AVAILABLE; 17 | } 18 | nvm_write((void *) &N_storage.metadatas[offset + 2], data, dataSize); 19 | uint8_t tmp[2]; 20 | tmp[0] = 0; 21 | tmp[1] = META_NONE; 22 | nvm_write((void *) &N_storage.metadatas[offset + 2 + dataSize], tmp, 2); 23 | tmp[0] = dataSize; 24 | tmp[1] = META_NONE; 25 | nvm_write((void *) &N_storage.metadatas[offset], tmp, 2); 26 | size_t metadata_count = N_storage.metadata_count + 1; 27 | nvm_write((void *) &N_storage.metadata_count, &metadata_count, 4); 28 | return OK; 29 | } 30 | 31 | void override_metadatas(uint8_t offset, void *ptr, size_t size) { 32 | nvm_write((void *) &N_storage.metadatas[offset], ptr, size); 33 | } 34 | 35 | void reset_metadatas(void) { 36 | nvm_write((void *) N_storage.metadatas, NULL, sizeof(N_storage.metadatas)); 37 | nvm_write((void *) &N_storage.metadata_count, 0, sizeof(N_storage.metadata_count)); 38 | } 39 | 40 | error_type_t erase_metadata(uint32_t offset) { 41 | if (N_storage.metadata_count == 0) { 42 | return ERR_NO_METADATA; 43 | } 44 | size_t metadata_count = N_storage.metadata_count - 1; 45 | unsigned char m = META_ERASED; 46 | nvm_write((void *) &N_storage.metadatas[offset + 1], &m, sizeof(N_storage.metadatas[0])); 47 | nvm_write((void *) &N_storage.metadata_count, 48 | &metadata_count, 49 | sizeof(N_storage.metadata_count)); 50 | return OK; 51 | } 52 | 53 | uint32_t find_free_metadata(void) { 54 | uint32_t offset = 0; 55 | while ((METADATA_DATALEN(offset) != 0) && (offset < MAX_METADATAS)) { 56 | offset += METADATA_TOTAL_LEN(offset); 57 | } 58 | return offset; 59 | } 60 | 61 | uint32_t get_metadata(uint32_t nth) { 62 | unsigned int offset = 0; 63 | for (;;) { 64 | if (METADATA_DATALEN(offset) == 0) { 65 | return -1UL; // end of file 66 | } 67 | if (METADATA_KIND(offset) != META_ERASED) { 68 | if (nth == 0) { 69 | return offset; 70 | } 71 | nth--; 72 | } 73 | offset += METADATA_TOTAL_LEN(offset); 74 | } 75 | } 76 | 77 | error_type_t compact_metadata() { 78 | uint32_t offset = 0; 79 | uint32_t shift_offset = 0; 80 | uint8_t copy_buffer[2 + 1 + MAX_METANAME]; 81 | while ((METADATA_DATALEN(offset) != 0) && (offset < MAX_METADATAS)) { 82 | if (METADATA_TOTAL_LEN(offset) >= sizeof(copy_buffer)) { 83 | return ERR_METADATA_ENTRY_TOO_BIG; 84 | } 85 | switch (METADATA_KIND(offset)) { 86 | case META_NONE: 87 | if (shift_offset != 0) { 88 | memcpy(copy_buffer, 89 | (const void *) METADATA_PTR(offset), 90 | METADATA_TOTAL_LEN(offset)); 91 | nvm_write((void *) &N_storage.metadatas[shift_offset], 92 | copy_buffer, 93 | METADATA_TOTAL_LEN(offset)); 94 | offset += METADATA_TOTAL_LEN(shift_offset); 95 | shift_offset += METADATA_TOTAL_LEN(shift_offset); 96 | } else { 97 | offset += METADATA_TOTAL_LEN(offset); 98 | } 99 | break; 100 | case META_ERASED: 101 | shift_offset = shift_offset == 0 ? offset : shift_offset; 102 | offset += METADATA_TOTAL_LEN(offset); 103 | break; 104 | 105 | default: 106 | return ERR_CORRUPTED_METADATA; 107 | } 108 | } 109 | if (shift_offset >= MAX_METADATAS || offset >= MAX_METADATAS) { 110 | return ERR_NO_MORE_SPACE_AVAILABLE; 111 | } 112 | // declare that the remaining space is free 113 | if (shift_offset != 0) { 114 | copy_buffer[0] = 0; 115 | copy_buffer[1] = META_NONE; 116 | nvm_write((void *) &N_storage.metadatas[shift_offset], copy_buffer, 2); 117 | } 118 | // count metadatas 119 | offset = 0; 120 | size_t count = 0; 121 | while ((METADATA_DATALEN(offset) != 0) && (offset < MAX_METADATAS)) { 122 | offset += METADATA_TOTAL_LEN(offset); 123 | count++; 124 | } 125 | nvm_write((void *) &N_storage.metadata_count, 126 | (void *) &count, 127 | sizeof(N_storage.metadata_count)); 128 | return OK; 129 | } 130 | -------------------------------------------------------------------------------- /src/metadata.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "error.h" 6 | #include "types.h" 7 | 8 | #define METADATA_PTR(offset) (&N_storage.metadatas[offset]) 9 | #define METADATA_TOTAL_LEN(offset) (METADATA_DATALEN(offset) + 2) 10 | #define METADATA_DATALEN(offset) N_storage.metadatas[offset] // charsets(1) + pwd seed(n) 11 | #define METADATA_KIND(offset) N_storage.metadatas[offset + 1] 12 | #define METADATA_SETS(offset) N_storage.metadatas[offset + 2] 13 | /* even if the database is corrupted, this guarantees we never overflow buffers of size 14 | * MAX_METANAME */ 15 | #define METADATA_NICKNAME_LEN(offset) ((METADATA_DATALEN(offset) - 1) % (MAX_METANAME + 1)) 16 | #define METADATA_NICKNAME(offset) (&N_storage.metadatas[offset + 3]) 17 | 18 | #define META_NONE 0x00 19 | #define META_ERASED 0xFF 20 | 21 | error_type_t write_metadata(uint8_t *data, uint8_t dataSize); 22 | 23 | /* 24 | * Write a given amount of data on metadatas, at the given offset 25 | * Used to load metadata from APDUs 26 | */ 27 | void override_metadatas(uint8_t offset, void *ptr, size_t size); 28 | 29 | void reset_metadatas(void); 30 | error_type_t erase_metadata(uint32_t offset); 31 | uint32_t find_free_metadata(void); 32 | uint32_t get_metadata(uint32_t nth); 33 | error_type_t compact_metadata(); 34 | -------------------------------------------------------------------------------- /src/options.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "options.h" 5 | #include "globals.h" 6 | 7 | #if !defined(TARGET_STAX) 8 | 9 | static uint8_t charset_options; 10 | 11 | static void set_charset_options(uint8_t value) { 12 | charset_options = value; 13 | } 14 | 15 | uint8_t get_charset_options() { 16 | return charset_options; 17 | } 18 | 19 | #else 20 | 21 | static void set_charset_options(uint8_t value) { 22 | nvm_write((void*) &N_storage.charset_options, (void*) &value, sizeof(value)); 23 | } 24 | 25 | uint8_t get_charset_options() { 26 | return N_storage.charset_options; 27 | } 28 | 29 | #endif // !defined(TARGET_STAX) 30 | 31 | void init_charset_options() { 32 | // default: uppercase (1) + lowercase (2) + numbers (4) = 7 33 | set_charset_options(0x07); 34 | } 35 | 36 | bool has_charset_option(const uint8_t bitflag) { 37 | return (get_charset_options() & bitflag) != 0; 38 | } 39 | 40 | void set_charset_option(const uint8_t bitflag) { 41 | set_charset_options(get_charset_options() ^ bitflag); 42 | } 43 | 44 | void change_enter_options() { 45 | bool new_value = !N_storage.press_enter_after_typing; 46 | nvm_write((void*) &N_storage.press_enter_after_typing, (void*) &new_value, sizeof(new_value)); 47 | } 48 | 49 | bool set_keyboard_layout(hid_mapping_t mapping) { 50 | const bool return_value = (N_storage.keyboard_layout == 0); 51 | nvm_write((void*) &N_storage.keyboard_layout, (void*) &mapping, sizeof(hid_mapping_t)); 52 | return return_value; 53 | } 54 | -------------------------------------------------------------------------------- /src/options.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | enum { 8 | UPPERCASE_BITFLAG = 1, 9 | LOWERCASE_BITFLAG = 2, 10 | NUMBERS_BITFLAG = 4, 11 | BARS_BITFLAG = 8 | 16 | 32, 12 | EXT_SYMBOLS_BITFLAG = 64 | 128, 13 | }; 14 | 15 | void init_charset_options(void); 16 | bool has_charset_option(const uint8_t bitflag); 17 | uint8_t get_charset_options(void); 18 | void set_charset_option(const uint8_t bitflag); 19 | void change_enter_options(); 20 | 21 | /* 22 | * Store the keyboard layout in NVM 23 | * Returns if it's the first time a layout is stored (true) or not (false) 24 | */ 25 | bool set_keyboard_layout(hid_mapping_t mapping); 26 | -------------------------------------------------------------------------------- /src/password.c: -------------------------------------------------------------------------------- 1 | #include "globals.h" 2 | #include "options.h" 3 | #include "metadata.h" 4 | #include "password.h" 5 | #include "password_typing.h" 6 | 7 | error_type_t create_new_password(const char* const pwd_name, const size_t pwd_size) { 8 | // use the G_io_seproxyhal_spi_buffer as temp buffer to build the entry (and include the 9 | // requested set of chars) 10 | memmove(G_io_seproxyhal_spi_buffer + 1, pwd_name, pwd_size); 11 | // use the requested classes from the user 12 | G_io_seproxyhal_spi_buffer[0] = get_charset_options(); 13 | // add the metadata 14 | return write_metadata(G_io_seproxyhal_spi_buffer, 1 + pwd_size); 15 | } 16 | 17 | void type_password_at_offset(const size_t offset) { 18 | unsigned char enabledSets = METADATA_SETS(offset); 19 | if (enabledSets == 0) { 20 | enabledSets = ALL_SETS; 21 | } 22 | type_password((uint8_t*) METADATA_NICKNAME(offset), 23 | METADATA_NICKNAME_LEN(offset), 24 | NULL, 25 | enabledSets, 26 | (const uint8_t*) PIC(DEFAULT_MIN_SET), 27 | PASSWORD_MAX_SIZE); 28 | } 29 | 30 | void show_password_at_offset(const size_t offset, uint8_t* dest_buffer) { 31 | unsigned char enabledSets = METADATA_SETS(offset); 32 | if (enabledSets == 0) { 33 | enabledSets = ALL_SETS; 34 | } 35 | type_password((uint8_t*) METADATA_NICKNAME(offset), 36 | METADATA_NICKNAME_LEN(offset), 37 | dest_buffer, 38 | enabledSets, 39 | (const uint8_t*) PIC(DEFAULT_MIN_SET), 40 | PASSWORD_MAX_SIZE); 41 | } 42 | 43 | error_type_t delete_password_at_offset(const size_t offset) { 44 | return erase_metadata(offset); 45 | } 46 | -------------------------------------------------------------------------------- /src/password.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define PASSWORD_MAX_SIZE 20 7 | 8 | /* 9 | * Inserts a new password into the storage. 10 | * `pwd_size` should not include the string last null-byte. 11 | */ 12 | error_type_t create_new_password(const char* const pwd_name, const size_t pwd_size); 13 | void type_password_at_offset(const size_t offset); 14 | void show_password_at_offset(const size_t offset, uint8_t* dest_buffer); 15 | error_type_t delete_password_at_offset(const size_t offset); 16 | -------------------------------------------------------------------------------- /src/password_generation.c: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Password Manager application 3 | * (c) 2017-2023 Ledger SAS 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ********************************************************************************/ 17 | 18 | #include 19 | #include 20 | 21 | static const char *SETS[] = {"ABCDEFGHIJKLMNOPQRSTUVWXYZ", // 26 22 | "abcdefghijklmnopqrstuvwxyz", // 26 23 | "0123456789", // 10 24 | "-", 25 | "_", 26 | " ", 27 | "\"#$%&'*+,./:;=?!@\\^`|~", // 22 28 | "[]{}()<>", // 8 29 | NULL}; 30 | 31 | static uint8_t rng_u8_modulo(mbedtls_ctr_drbg_context *drbg, uint8_t modulo) { 32 | if (modulo == 0) { 33 | THROW(EXCEPTION); 34 | } 35 | uint32_t rng_max = 256 % modulo; 36 | uint32_t rng_limit = 256 - rng_max; 37 | uint8_t candidate = 0; 38 | do { 39 | if (mbedtls_ctr_drbg_random(drbg, &candidate, 1) != 0) { 40 | THROW(EXCEPTION); 41 | } 42 | } while (candidate > rng_limit); 43 | // PRINTF("r:%02X ", candidate); 44 | return (candidate % modulo); 45 | } 46 | 47 | static void shuffle_array(mbedtls_ctr_drbg_context *drbg, uint8_t *buffer, uint32_t size) { 48 | uint32_t i; 49 | for (i = size - 1; i > 0; i--) { 50 | uint32_t index = rng_u8_modulo(drbg, i + 1); 51 | uint8_t tmp = buffer[i]; 52 | buffer[i] = buffer[index]; 53 | buffer[index] = tmp; 54 | } 55 | } 56 | 57 | /* Sample from set with replacement */ 58 | static void sample(mbedtls_ctr_drbg_context *drbg, 59 | const uint8_t *set, 60 | uint32_t setSize, 61 | uint8_t *out, 62 | uint32_t size) { 63 | uint32_t i; 64 | for (i = 0; i < size; i++) { 65 | uint32_t index = rng_u8_modulo(drbg, setSize); 66 | out[i] = set[index]; 67 | } 68 | } 69 | 70 | uint32_t generate_password(mbedtls_ctr_drbg_context *drbg, 71 | setmask_t setMask, 72 | const uint8_t *minFromSet, 73 | uint8_t *out, 74 | uint32_t size) { 75 | uint8_t setChars[100]; 76 | uint32_t setCharsOffset = 0; 77 | uint32_t outOffset = 0; 78 | uint32_t i; 79 | 80 | for (i = 0; setMask && i < NUM_SETS; i++, setMask >>= 1) { 81 | if (setMask & 1) { 82 | const uint8_t *set = (const uint8_t *) PIC(SETS[i]); 83 | uint32_t setSize = strlen((const char *) set); 84 | memcpy(setChars + setCharsOffset, set, setSize); 85 | setCharsOffset += setSize; 86 | 87 | // for at least requested minimum chars from that set 88 | if (minFromSet[i] > 0) { 89 | if (outOffset + minFromSet[i] > size) { 90 | THROW(EXCEPTION); 91 | } 92 | sample(drbg, set, setSize, out + outOffset, minFromSet[i]); 93 | outOffset += minFromSet[i]; 94 | } 95 | } 96 | } 97 | 98 | if (setMask || setCharsOffset == 0 || setCharsOffset >= sizeof(setChars)) { 99 | THROW(EXCEPTION); 100 | } 101 | 102 | // PRINTF("chars from: %.*H\n", setCharsOffset, setChars); 103 | 104 | sample(drbg, setChars, setCharsOffset, out + outOffset, size - outOffset); 105 | // PRINTF("selected: %.*H\n", size, out); 106 | shuffle_array(drbg, out, size); 107 | out[size] = '\0'; 108 | return size; 109 | } 110 | -------------------------------------------------------------------------------- /src/password_typing.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #ifdef REVAMPED_IO 9 | #include "usbd_ledger.h" 10 | #endif // REVAMPED_IO 11 | 12 | #include "password_typing.h" 13 | #include "globals.h" 14 | 15 | #define REPORT_SIZE 8 16 | static const uint8_t EMPTY_REPORT[REPORT_SIZE] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 17 | static const uint8_t SPACE_REPORT[REPORT_SIZE] = {0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00}; 18 | static const uint8_t CAPS_REPORT[REPORT_SIZE] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 19 | static const uint8_t CAPS_LOCK_REPORT[REPORT_SIZE] = 20 | {0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00}; 21 | static const uint8_t ENTER_REPORT[REPORT_SIZE] = {0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00}; 22 | 23 | bool entropyProvided; 24 | uint8_t entropy[32]; 25 | 26 | static int entropyProvider2(__attribute__((unused)) void *context, 27 | unsigned char *buffer, 28 | __attribute__((unused)) size_t bufferSize) { 29 | if (entropyProvided) { 30 | return 1; 31 | } 32 | memcpy(buffer, entropy, 32); 33 | entropyProvided = true; 34 | return 0; 35 | } 36 | 37 | #ifndef TESTING 38 | #ifdef REVAMPED_IO 39 | static void usb_write_wait(unsigned char *buf) { 40 | USBD_LEDGER_send(USBD_LEDGER_CLASS_HID_KBD, 0, buf, REPORT_SIZE, 0); 41 | os_io_seph_cmd_general_status(); 42 | } 43 | #else // REVAMPED_IO 44 | static void usb_write_wait(unsigned char *buf) { 45 | io_usb_send_ep(HID_EPIN_ADDR, buf, REPORT_SIZE, 60); 46 | 47 | // wait until transfer timeout, or ended 48 | while (G_io_app.usb_ep_timeouts[HID_EPIN_ADDR & 0x7F].timeout) { 49 | if (!io_seproxyhal_spi_is_status_sent()) { 50 | io_seproxyhal_general_status(); 51 | } 52 | io_seproxyhal_spi_recv(G_io_seproxyhal_spi_buffer, sizeof(G_io_seproxyhal_spi_buffer), 0); 53 | io_seproxyhal_handle_event(); 54 | } 55 | } 56 | #endif // !REVAMPED_IO 57 | #else 58 | static void usb_write_wait(__attribute__((unused)) unsigned char *buf) { 59 | return; 60 | } 61 | #endif // TESTING 62 | 63 | bool type_password(uint8_t *data, 64 | uint32_t dataSize, 65 | uint8_t *out, 66 | setmask_t setMask, 67 | const uint8_t *minFromSet, 68 | uint32_t size) { 69 | uint32_t derive[9]; 70 | uint32_t led_status; 71 | uint8_t tmp[64]; 72 | uint8_t i; 73 | uint8_t report[REPORT_SIZE] = {0}; 74 | 75 | cx_hash_sha256(data, dataSize, tmp, sizeof(tmp)); 76 | derive[0] = DERIVE_PASSWORD_PATH; 77 | for (i = 0; i < 8; i++) { 78 | derive[i + 1] = 0x80000000 | (tmp[4 * i] << 24) | (tmp[4 * i + 1] << 16) | 79 | (tmp[4 * i + 2] << 8) | (tmp[4 * i + 3]); 80 | } 81 | 82 | if (os_derive_bip32_no_throw(CX_CURVE_SECP256K1, derive, 9, tmp, tmp + 32) != CX_OK) { 83 | return false; 84 | } 85 | cx_hash_sha256(tmp, 64, entropy, sizeof(entropy)); 86 | memset(tmp, 0, sizeof(tmp)); 87 | entropyProvided = false; 88 | mbedtls_ctr_drbg_context ctx; 89 | mbedtls_ctr_drbg_init(&ctx); 90 | if (mbedtls_ctr_drbg_seed(&ctx, entropyProvider2, NULL, NULL, 0) != 0) { 91 | THROW(EXCEPTION); 92 | } 93 | if (out != NULL) { 94 | generate_password(&ctx, setMask, minFromSet, out, size); 95 | return true; 96 | } 97 | 98 | generate_password(&ctx, setMask, minFromSet, tmp, size); 99 | 100 | led_status = G_led_status; 101 | 102 | // Insert EMPTY_REPORT CAPS_REPORT EMPTY_REPORT to avoid undesired capital letter on KONSOLE 103 | usb_write_wait((uint8_t *) EMPTY_REPORT); 104 | usb_write_wait((uint8_t *) CAPS_REPORT); 105 | usb_write_wait((uint8_t *) EMPTY_REPORT); 106 | 107 | // toggle shift if set. 108 | if (led_status & 2) { 109 | usb_write_wait((uint8_t *) CAPS_LOCK_REPORT); 110 | usb_write_wait((uint8_t *) EMPTY_REPORT); 111 | } 112 | 113 | for (i = 0; i < size; i++) { 114 | // If keyboard layout not initialized, use the default 115 | map_char(N_storage.keyboard_layout, tmp[i], report); 116 | 117 | usb_write_wait(report); 118 | if (report[0] & SHIFT_KEY) { 119 | usb_write_wait((uint8_t *) CAPS_REPORT); 120 | } else { 121 | usb_write_wait((uint8_t *) EMPTY_REPORT); 122 | } 123 | 124 | // for international keyboard, make sure to insert space after special symbols 125 | if (N_storage.keyboard_layout == HID_MAPPING_QWERTY_INTL) { 126 | switch (tmp[i]) { 127 | case '\"': 128 | case '\'': 129 | case '`': 130 | case '~': 131 | case '^': 132 | // insert a extra space to validate the symbol 133 | usb_write_wait((uint8_t *) SPACE_REPORT); 134 | usb_write_wait((uint8_t *) EMPTY_REPORT); 135 | break; 136 | } 137 | } 138 | } 139 | usb_write_wait((uint8_t *) EMPTY_REPORT); 140 | // restore shift state 141 | if (led_status & 2) { 142 | usb_write_wait((uint8_t *) CAPS_LOCK_REPORT); 143 | usb_write_wait((uint8_t *) EMPTY_REPORT); 144 | } 145 | 146 | if (N_storage.press_enter_after_typing) { 147 | // press enter 148 | usb_write_wait((uint8_t *) ENTER_REPORT); 149 | usb_write_wait((uint8_t *) EMPTY_REPORT); 150 | } 151 | 152 | return true; 153 | } 154 | -------------------------------------------------------------------------------- /src/password_typing.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | bool type_password(uint8_t *data, 8 | uint32_t dataSize, 9 | uint8_t *out, 10 | setmask_t setMask, 11 | const uint8_t *minFromSet, 12 | uint32_t size); 13 | 14 | #define DERIVE_PASSWORD_PATH 0x80505744 15 | 16 | static const uint8_t DEFAULT_MIN_SET[] = {1, 1, 1, 0, 0, 1, 0, 0}; 17 | -------------------------------------------------------------------------------- /src/stax/password_list.c: -------------------------------------------------------------------------------- 1 | #if defined(TEST) 2 | #include 3 | #else 4 | #include 5 | #endif 6 | 7 | #include "password_list.h" 8 | 9 | typedef struct passwordList_s { 10 | /* 11 | * Buffer where password names are stored 12 | */ 13 | char buffer[DISPLAYED_PASSWORD_PER_PAGE * (MAX_METANAME + 1)]; 14 | /* 15 | * Used to keep track of password store in buffer. Start at 0, is incremented with password 16 | * length (including trailing '\0' each time a password is stored. 17 | */ 18 | size_t currentOffset; 19 | /* 20 | * Placeholder for currently displayed password names, points to previous `buffer` locations. 21 | * Also used to display currently selected password name (for display or deletion) 22 | */ 23 | const char *passwords[DISPLAYED_PASSWORD_PER_PAGE]; 24 | /* 25 | * Like `passwords` keeps a relation between currently displayed password indexes and their 26 | * names, this array keeps the relation between currently displayed password indexes and their 27 | * offset in metadatas. 28 | */ 29 | size_t offsets[DISPLAYED_PASSWORD_PER_PAGE]; 30 | /* 31 | * Used to pin an index in order to retrieve the related offset with 32 | * `password_list_get_current_offset`. Needed as the offset is used in a callback function with 33 | * no meaningful arguments. 34 | */ 35 | size_t index; 36 | } passwordList_t; 37 | 38 | static passwordList_t passwordList = {0}; 39 | 40 | void password_list_reset() { 41 | explicit_bzero(&passwordList, sizeof(passwordList)); 42 | } 43 | 44 | size_t password_list_get_offset(const size_t index) { 45 | if (index >= DISPLAYED_PASSWORD_PER_PAGE) { 46 | return -1; 47 | } 48 | return passwordList.offsets[index]; 49 | } 50 | 51 | size_t password_list_get_current_offset() { 52 | return passwordList.offsets[passwordList.index]; 53 | } 54 | 55 | const char *password_list_get_password(const size_t index) { 56 | if (index >= DISPLAYED_PASSWORD_PER_PAGE) { 57 | return NULL; 58 | } 59 | return passwordList.passwords[index]; 60 | } 61 | 62 | void password_list_set_current(const size_t index) { 63 | passwordList.index = index; 64 | } 65 | 66 | bool password_list_add_password(const size_t index, 67 | const size_t offset, 68 | const char *const password, 69 | const size_t length) { 70 | if (index >= DISPLAYED_PASSWORD_PER_PAGE) { 71 | return false; 72 | } 73 | passwordList.offsets[index] = offset; 74 | void *nextPwdPtr = &passwordList.buffer[0] + passwordList.currentOffset; 75 | strlcpy(nextPwdPtr, password, length); 76 | passwordList.passwords[index] = nextPwdPtr; 77 | passwordList.currentOffset += length; 78 | return true; 79 | } 80 | 81 | void password_list_reset_buffer() { 82 | explicit_bzero(&passwordList.buffer[0], sizeof(passwordList.buffer)); 83 | passwordList.currentOffset = 0; 84 | } 85 | 86 | const char *const *password_list_passwords() { 87 | return passwordList.passwords; 88 | } 89 | -------------------------------------------------------------------------------- /src/stax/password_list.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define DISPLAYED_PASSWORD_PER_PAGE 5 6 | 7 | #if defined(TARGET_STAX) 8 | 9 | void password_list_reset(); 10 | 11 | size_t password_list_get_offset(const size_t index); 12 | 13 | const char *password_list_get_password(const size_t index); 14 | 15 | bool password_list_add_password(const size_t index, 16 | const size_t offset, 17 | const char *const password, 18 | const size_t length); 19 | 20 | void password_list_set_current(const size_t index); 21 | 22 | size_t password_list_get_current_offset(); 23 | 24 | void password_list_reset_buffer(); 25 | 26 | const char *const *password_list_passwords(); 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /src/tests/tests.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "error.h" 4 | #include "password_typing.h" 5 | #include "tests.h" 6 | 7 | /* Takes a metadata as an input (charset + seed) and returns a 20 char password*/ 8 | int test_generate_password(const buf_t *input) { 9 | uint8_t enabledSets = input->bytes[0]; 10 | if (enabledSets == 0) { 11 | enabledSets = ALL_SETS; 12 | } 13 | uint8_t *seed_ptr = input->bytes + 1; 14 | size_t seed_len = input->size - 1; 15 | uint8_t out_buffer[20]; 16 | type_password(seed_ptr, 17 | seed_len, 18 | out_buffer, 19 | enabledSets, 20 | (const uint8_t *) PIC(DEFAULT_MIN_SET), 21 | sizeof(out_buffer)); 22 | return io_send_response_pointer(out_buffer, 20, SW_OK); 23 | } 24 | 25 | int test_dispatcher(uint8_t p1, __attribute__((unused)) uint8_t p2, const buf_t *input) { 26 | switch (p1) { 27 | case GENERATE_PASSWORD: 28 | return test_generate_password(input); 29 | default: 30 | return io_send_sw(SW_INS_NOT_SUPPORTED + 1); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/tests/tests.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "stdint.h" 4 | #include "types.h" 5 | 6 | typedef enum { 7 | GENERATE_PASSWORD = 0x01, 8 | } test_cmd_e; 9 | 10 | int test_dispatcher(uint8_t p1, uint8_t p2, const buf_t *input); 11 | -------------------------------------------------------------------------------- /src/types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct internalStorage_t { 9 | #define STORAGE_MAGIC 0xDEAD1337 10 | uint32_t magic; 11 | bool press_enter_after_typing; 12 | uint8_t keyboard_layout; 13 | /** 14 | * A metadata in memory is represented by 1 byte of size (l), 1 byte of type (to disable it if 15 | * required), 1 byte to select char sets, l bytes of user seed 16 | */ 17 | size_t metadata_count; 18 | uint8_t metadatas[MAX_METADATAS]; 19 | #if defined(TARGET_STAX) 20 | uint8_t charset_options; 21 | #endif 22 | } internalStorage_t; 23 | 24 | typedef enum { 25 | GET_APP_CONFIG = 0x03, 26 | DUMP_METADATAS = 0x04, 27 | LOAD_METADATAS = 0x05, 28 | #ifdef TESTING 29 | RUN_TEST = 0x99 30 | #endif 31 | } cmd_e; 32 | 33 | typedef struct app_state_s { 34 | size_t output_len; 35 | cmd_e current_command; 36 | size_t bytes_transferred; 37 | bool user_approval; 38 | } app_state_t; 39 | 40 | typedef struct { 41 | uint8_t* bytes; 42 | size_t size; 43 | } buf_t; 44 | 45 | typedef struct message_pair_s { 46 | const char* first; 47 | const char* second; 48 | } message_pair_t; 49 | -------------------------------------------------------------------------------- /src/ui.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "types.h" 6 | 7 | void ui_idle(); 8 | void ui_request_user_approval(message_pair_t *msg); 9 | -------------------------------------------------------------------------------- /tests/functional/automation.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "rules": [ 4 | { 5 | "regexp": "Transfer|Overwrite", 6 | "conditions": [ 7 | [ "seen", false ] 8 | ], 9 | "actions": [ 10 | [ "setbool", "seen", true ] 11 | ] 12 | }, 13 | { 14 | "text": "metadatas ?", 15 | "conditions": [ 16 | [ "seen", true ] 17 | ], 18 | "actions": [ 19 | [ "button", 1, true ], 20 | [ "button", 2, true ], 21 | [ "button", 1, false ], 22 | [ "button", 2, false ], 23 | [ "setbool", "seen", false ] 24 | ] 25 | } 26 | ] 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/functional/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pathlib import Path 3 | from ragger.backend import RaisePolicy 4 | 5 | from ledgered.devices import Device, Devices 6 | from passwordsManager_cmd import PasswordsManagerCommand 7 | from tests_vectors import tests_vectors 8 | from stax.navigator import CustomStaxNavigator 9 | 10 | pytest_plugins = ("ragger.conftest.base_conftest", ) 11 | 12 | 13 | # Glue to call every test that depends on the device once for each required device 14 | def pytest_generate_tests(metafunc): 15 | if "test_vector" in metafunc.fixturenames: 16 | metafunc.parametrize( 17 | "test_vector", tests_vectors[metafunc.definition.name]) 18 | 19 | 20 | @pytest.fixture(scope="session") 21 | def functional_test_directory(root_pytest_dir) -> Path: 22 | return root_pytest_dir / "tests" / "functional" 23 | 24 | 25 | @pytest.fixture 26 | def custom_backend(backend): 27 | backend.raise_policy = RaisePolicy.RAISE_NOTHING 28 | yield backend 29 | 30 | 31 | @pytest.fixture 32 | def cmd(custom_backend, device): 33 | command = PasswordsManagerCommand( 34 | transport=custom_backend, 35 | device=device, 36 | debug=True 37 | ) 38 | yield command 39 | 40 | 41 | @pytest.fixture 42 | def navigator(custom_backend, device, golden_run): 43 | navigator = CustomStaxNavigator(custom_backend, device, golden_run) 44 | yield navigator 45 | 46 | 47 | @pytest.fixture(autouse=True) 48 | def use_on_device(request, device: Device): 49 | if request.node.get_closest_marker('use_on_device'): 50 | current_device = request.node.get_closest_marker('use_on_device').args[0].lower() 51 | if Devices.get_by_name(current_device).type != device.type: 52 | pytest.skip(f'skipped on this device: "{device}" is not '\ 53 | f'"{current_device}"') 54 | 55 | 56 | def pytest_configure(config): 57 | config.addinivalue_line( 58 | "markers", 59 | "use_on_device(device): skip test if not on the specified device", 60 | ) 61 | config.addinivalue_line( 62 | "markers", 63 | "requires_physical_device(): skip test if not on a physical device" 64 | ) 65 | -------------------------------------------------------------------------------- /tests/functional/exception/__init__.py: -------------------------------------------------------------------------------- 1 | from .device_exception import DeviceException 2 | from .types import (UnknownDeviceError, 3 | WrongP1P2Error, 4 | WrongDataLengthError, 5 | InsNotSupportedError, 6 | ClaNotSupportedError, 7 | AppNameTooLongError, 8 | ActionCancelledError, 9 | MetadatasParsingError) 10 | 11 | __all__ = [ 12 | "DeviceException", 13 | "UnknownDeviceError", 14 | "WrongP1P2Error", 15 | "WrongDataLengthError", 16 | "InsNotSupportedError", 17 | "ClaNotSupportedError", 18 | "AppNameTooLongError", 19 | "ActionCancelledError", 20 | "MetadatasParsingError" 21 | ] 22 | -------------------------------------------------------------------------------- /tests/functional/exception/device_exception.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from typing import Dict, Any, Optional 3 | 4 | from .types import WrongP1P2Error, WrongDataLengthError, InsNotSupportedError, \ 5 | ClaNotSupportedError, AppNameTooLongError, ActionCancelledError, MetadatasParsingError, \ 6 | UnknownDeviceError 7 | 8 | class DeviceException(Exception): # pylint: disable=too-few-public-methods 9 | exc: Dict[int, Any] = { 10 | 0x6A86: WrongP1P2Error, 11 | 0x6A87: WrongDataLengthError, 12 | 0x6D00: InsNotSupportedError, 13 | 0x6E00: ClaNotSupportedError, 14 | 0xB000: AppNameTooLongError, 15 | 0x6985: ActionCancelledError, 16 | 0x6F10: MetadatasParsingError 17 | } 18 | 19 | def __new__(cls, 20 | error_code: int, 21 | ins: Optional[enum.IntEnum] = None, 22 | message: str = "" 23 | ) -> Any: 24 | error_message: str = (f"Error in {ins!r} command" 25 | if ins else "Error in command") 26 | 27 | if error_code in DeviceException.exc: 28 | return DeviceException.exc[error_code](hex(error_code), 29 | error_message, 30 | message) 31 | 32 | return UnknownDeviceError(hex(error_code), error_message, message) 33 | -------------------------------------------------------------------------------- /tests/functional/exception/types.py: -------------------------------------------------------------------------------- 1 | class UnknownDeviceError(Exception): 2 | pass 3 | 4 | 5 | class WrongP1P2Error(Exception): 6 | pass 7 | 8 | 9 | class WrongDataLengthError(Exception): 10 | pass 11 | 12 | 13 | class InsNotSupportedError(Exception): 14 | pass 15 | 16 | 17 | class ClaNotSupportedError(Exception): 18 | pass 19 | 20 | 21 | class AppNameTooLongError(Exception): 22 | pass 23 | 24 | 25 | class ActionCancelledError(Exception): 26 | pass 27 | 28 | 29 | class MetadatasParsingError(Exception): 30 | pass 31 | -------------------------------------------------------------------------------- /tests/functional/passwordsManager_cmd.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | from exception import DeviceException 4 | from ledgered.devices import Device 5 | from ragger.backend import BackendInterface 6 | from ragger.firmware.touch.positions import STAX_BUTTON_ABOVE_LOWER_MIDDLE 7 | 8 | CLA_SDK: int = 0xb0 9 | CLA: int = 0xe0 10 | 11 | 12 | class InsType(enum.IntEnum): 13 | INS_GET_APP_INFO = 0x01 14 | INS_GET_APP_CONFIG = 0x03 15 | INS_DUMP_METADATAS = 0x04 16 | INS_LOAD_METADATAS = 0x05 17 | INS_RUN_TEST = 0x99 18 | 19 | 20 | class TestInsType(enum.IntEnum): 21 | INS_GENERATE_PASSWORD = 0x01 22 | 23 | 24 | class PasswordsManagerCommand: 25 | def __init__(self, 26 | transport: BackendInterface, 27 | device: Device, 28 | debug: bool = False) -> None: 29 | self.transport = transport 30 | self.device = device 31 | self.debug = debug 32 | self.approved: bool = False 33 | 34 | def approve(self): 35 | if self.device.touchable: 36 | self.transport.finger_touch(*STAX_BUTTON_ABOVE_LOWER_MIDDLE) 37 | else: 38 | self.transport.both_click() 39 | 40 | def get_app_info(self) -> str: 41 | ins: InsType = InsType.INS_GET_APP_INFO 42 | 43 | response = self.transport.exchange(cla=CLA_SDK, ins=ins) 44 | sw, response = response.status, response.data 45 | 46 | if not sw & 0x9000: 47 | raise DeviceException(error_code=sw, ins=ins) 48 | 49 | offset = 1 50 | app_name_length = response[offset] 51 | offset += 1 52 | app_name = response[offset:offset+app_name_length].decode("ascii") 53 | offset += app_name_length 54 | app_version_length = response[offset] 55 | offset += 1 56 | app_version = response[offset:offset + 57 | app_version_length].decode("ascii") 58 | 59 | return app_name, app_version 60 | 61 | def get_app_config(self) -> str: 62 | ins: InsType = InsType.INS_GET_APP_CONFIG 63 | 64 | response =self.transport.exchange(cla=CLA, ins=ins) 65 | sw, response = response.status, response.data 66 | 67 | if not sw & 0x9000: 68 | raise DeviceException(error_code=sw, ins=ins) 69 | 70 | assert len(response) == 6 71 | 72 | storage_size = int.from_bytes(response[:4], "big") 73 | keyboard_type = response[4] 74 | press_enter_after_typing = response[5] 75 | 76 | return storage_size, keyboard_type, press_enter_after_typing 77 | 78 | def reset_approval_state(self): 79 | # dummy call just to reset internal approval state 80 | self.get_app_config() 81 | 82 | def generate_password(self, charsets: int, seed: str) -> str: 83 | assert charsets <= 0xFF 84 | assert len(seed) <= 20 85 | 86 | ins: InsType = InsType.INS_RUN_TEST 87 | testIns: TestInsType = TestInsType.INS_GENERATE_PASSWORD 88 | 89 | payload = charsets.to_bytes( 90 | 1, "big") + bytes(seed, "utf-8") 91 | 92 | response = self.transport.exchange(cla=CLA, 93 | ins=ins, 94 | p1=testIns, 95 | data=payload) 96 | sw, response = response.status, response.data 97 | 98 | if not sw & 0x9000: 99 | raise DeviceException(error_code=sw, ins=ins) 100 | 101 | return response.decode("ascii") 102 | 103 | def dump_metadatas(self, size) -> bytes: 104 | ins: InsType = InsType.INS_DUMP_METADATAS 105 | 106 | metadatas = b"" 107 | self.approved = False 108 | while len(metadatas) < size: 109 | if not self.approved: 110 | with self.transport.exchange_async(cla=CLA, ins=ins): 111 | self.approve() 112 | response = self.transport.last_async_response 113 | self.approved = True 114 | else: 115 | response = self.transport.exchange(cla=CLA, ins=ins) 116 | sw, response = response.status, response.data 117 | if not sw & 0x9000: 118 | raise DeviceException(error_code=sw, ins=ins) 119 | 120 | metadatas += response[1:] 121 | 122 | if response[0] == 0xFF and len(metadatas) < size: 123 | raise Exception( 124 | f"{size} bytes requested but only {len(metadatas)} bytes available") 125 | 126 | return metadatas[:size] 127 | 128 | def load_metadatas_chunk(self, chunk, is_last): 129 | ins: InsType = InsType.INS_LOAD_METADATAS 130 | if not self.approved: 131 | with self.transport.exchange_async(cla=CLA, 132 | ins=ins, 133 | p1=0xFF if is_last else 0x00, 134 | data=chunk): 135 | self.approve() 136 | response = self.transport.last_async_response 137 | self.approved = True 138 | else: 139 | response = self.transport.exchange(cla=CLA, 140 | ins=ins, 141 | p1=0xFF if is_last else 0x00, 142 | data=chunk) 143 | sw, response = response.status, response.data 144 | 145 | if not sw & 0x9000: 146 | raise DeviceException(error_code=sw, ins=ins) 147 | 148 | def load_metadatas(self, metadatas): 149 | 150 | chunks = [metadatas[i:i+255] for i in range(0, len(metadatas), 255)] 151 | 152 | self.approved = False 153 | for i, chunk in enumerate(chunks): 154 | is_last_chunk = True if i+1 == len(chunks) else False 155 | self.load_metadatas_chunk(chunk, is_last_chunk) 156 | -------------------------------------------------------------------------------- /tests/functional/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=6.1.1 2 | ledgered 3 | ragger[tests,speculos] 4 | -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/all_passwords_deleted_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/all_passwords_deleted_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/confirm_all_passwords_deletion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/confirm_all_passwords_deletion.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/confirm_password_deletion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/confirm_password_deletion.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00000.png: -------------------------------------------------------------------------------- 1 | ../disclaimer.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00001.png: -------------------------------------------------------------------------------- 1 | ../startup_choose_kbl.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00002.png: -------------------------------------------------------------------------------- 1 | ../home_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00003.png: -------------------------------------------------------------------------------- 1 | ../menu_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00004.png: -------------------------------------------------------------------------------- 1 | ../list_screen_populated.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00005.png: -------------------------------------------------------------------------------- 1 | ../menu_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00006.png: -------------------------------------------------------------------------------- 1 | ../keyboard_screen_empty.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00007.png: -------------------------------------------------------------------------------- 1 | ../keyboard_screen_n_text.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00008.png: -------------------------------------------------------------------------------- 1 | ../keyboard_screen_ne_text.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00009.png: -------------------------------------------------------------------------------- 1 | ../keyboard_screen_new_text.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00010.png: -------------------------------------------------------------------------------- 1 | ../password_created_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00011.png: -------------------------------------------------------------------------------- 1 | ../menu_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/create_password/00012.png: -------------------------------------------------------------------------------- 1 | ../list_screen_populated_and_new.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_all_password/00000.png: -------------------------------------------------------------------------------- 1 | ../disclaimer.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_all_password/00001.png: -------------------------------------------------------------------------------- 1 | ../startup_choose_kbl.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_all_password/00002.png: -------------------------------------------------------------------------------- 1 | ../home_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_all_password/00003.png: -------------------------------------------------------------------------------- 1 | ../menu_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_all_password/00004.png: -------------------------------------------------------------------------------- 1 | ../confirm_all_passwords_deletion.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_all_password/00005.png: -------------------------------------------------------------------------------- 1 | ../all_passwords_deleted_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_all_password/00006.png: -------------------------------------------------------------------------------- 1 | ../menu_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_all_password/00007.png: -------------------------------------------------------------------------------- 1 | ../list_screen_empty.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_one_password/00000.png: -------------------------------------------------------------------------------- 1 | ../disclaimer.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_one_password/00001.png: -------------------------------------------------------------------------------- 1 | ../startup_choose_kbl.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_one_password/00002.png: -------------------------------------------------------------------------------- 1 | ../home_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_one_password/00003.png: -------------------------------------------------------------------------------- 1 | ../menu_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_one_password/00004.png: -------------------------------------------------------------------------------- 1 | ../list_screen_populated.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_one_password/00005.png: -------------------------------------------------------------------------------- 1 | ../confirm_password_deletion.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_one_password/00006.png: -------------------------------------------------------------------------------- 1 | ../password_deleted_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_one_password/00007.png: -------------------------------------------------------------------------------- 1 | ../menu_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_one_password/00008.png: -------------------------------------------------------------------------------- 1 | ../list_screen_populated_one_deleted.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/delete_one_password/00009.png: -------------------------------------------------------------------------------- 1 | ../menu_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/disclaimer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/disclaimer.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/home_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/home_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/keyboard_screen_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/keyboard_screen_empty.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/keyboard_screen_n_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/keyboard_screen_n_text.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/keyboard_screen_ne_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/keyboard_screen_ne_text.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/keyboard_screen_new_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/keyboard_screen_new_text.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/list_screen_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/list_screen_empty.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/list_screen_populated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/list_screen_populated.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/list_screen_populated_and_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/list_screen_populated_and_new.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/list_screen_populated_one_deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/list_screen_populated_one_deleted.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/menu_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/menu_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/password_created_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/password_created_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/password_deleted_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/password_deleted_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/settings/00000.png: -------------------------------------------------------------------------------- 1 | ../disclaimer.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/settings/00001.png: -------------------------------------------------------------------------------- 1 | ../startup_choose_kbl.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/settings/00002.png: -------------------------------------------------------------------------------- 1 | ../home_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/settings/00003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/settings/00003.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/settings/00004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/settings/00004.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/settings/00005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/settings/00005.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/settings/00006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/settings/00006.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/settings/00007.png: -------------------------------------------------------------------------------- 1 | ../home_screen.png -------------------------------------------------------------------------------- /tests/functional/snapshots/stax/startup_choose_kbl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/snapshots/stax/startup_choose_kbl.png -------------------------------------------------------------------------------- /tests/functional/stax/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/app-passwords/392c74354e1d331a7f3804ef8f1f8cd684f82949/tests/functional/stax/__init__.py -------------------------------------------------------------------------------- /tests/functional/stax/navigator.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | from functools import partial 3 | from ragger.navigator import NavInsID, BaseNavInsID 4 | from ragger.navigator.navigator import Navigator 5 | from time import sleep 6 | 7 | from .screen import CustomStaxScreen 8 | 9 | 10 | class CustomNavInsID(BaseNavInsID): 11 | WAIT = auto() 12 | TOUCH = auto() 13 | # home screen 14 | HOME_TO_QUIT = auto() 15 | HOME_TO_SETTINGS = auto() 16 | HOME_TO_MENU = auto() 17 | # settings 18 | SETTINGS_TO_HOME = auto() 19 | SETTINGS_NEXT = auto() 20 | SETTINGS_PREVIOUS = auto() 21 | # menu 22 | MENU_TO_HOME = auto() 23 | MENU_TO_TYPE = auto() 24 | MENU_TO_DISPLAY = auto() 25 | MENU_TO_CREATE = auto() 26 | MENU_TO_DELETE = auto() 27 | MENU_TO_DELETE_ALL = auto() 28 | # option with a list of password (type, show, delete) 29 | LIST_TO_MENU = auto() 30 | LIST_NEXT = auto() 31 | LIST_PREVIOUS = auto() 32 | # option confirm (password deletion, all passwords deletion) 33 | CONFIRM_YES = auto() 34 | CONFIRM_NO = auto() 35 | # option with a keyboard (create password) 36 | KEYBOARD_WRITE = auto() 37 | KEYBOARD_TO_CONFIRM = auto() 38 | KEYBOARD_TO_MENU = auto() 39 | # choosing option in choice lists (settings, menu) 40 | LIST_CHOOSE = auto() 41 | # Keyboard layout selection 42 | CHOOSE_KBL_QWERTY = auto() 43 | CHOOSE_KBL_QWERTY_INTL = auto() 44 | CHOOSE_KBL_AZERTY = auto() 45 | # startup disclaimer 46 | DISCLAIMER_CONFIRM = auto() 47 | DISCLAIMER_REJECT = auto() 48 | 49 | 50 | class CustomStaxNavigator(Navigator): 51 | 52 | def __init__(self, backend, device, golden_run): 53 | self.screen = CustomStaxScreen(backend, device) 54 | callbacks = { 55 | # has to be defined for Ragger Navigator internals 56 | NavInsID.WAIT: sleep, 57 | CustomNavInsID.WAIT: sleep, 58 | CustomNavInsID.TOUCH: backend.finger_touch, 59 | CustomNavInsID.HOME_TO_SETTINGS: self.screen.home.settings, 60 | CustomNavInsID.HOME_TO_QUIT: self.screen.home.quit, 61 | CustomNavInsID.HOME_TO_MENU: self.screen.home.action, 62 | CustomNavInsID.SETTINGS_PREVIOUS: self.screen.settings.previous, 63 | CustomNavInsID.SETTINGS_TO_HOME: self.screen.settings.multi_page_exit, 64 | CustomNavInsID.SETTINGS_NEXT: self.screen.settings.next, 65 | CustomNavInsID.MENU_TO_HOME: self.screen.menu.single_page_exit, 66 | CustomNavInsID.MENU_TO_TYPE: partial(self.screen.menu_choice.choose, 1), 67 | CustomNavInsID.MENU_TO_DISPLAY: partial(self.screen.menu_choice.choose, 2), 68 | CustomNavInsID.MENU_TO_CREATE: partial(self.screen.menu_choice.choose, 3), 69 | CustomNavInsID.MENU_TO_DELETE: partial(self.screen.menu_choice.choose, 4), 70 | CustomNavInsID.MENU_TO_DELETE_ALL: partial(self.screen.menu_choice.choose, 5), 71 | CustomNavInsID.LIST_TO_MENU: self.screen.settings.multi_page_exit, 72 | CustomNavInsID.LIST_NEXT: self.screen.settings.next, 73 | CustomNavInsID.LIST_PREVIOUS: self.screen.settings.previous, 74 | CustomNavInsID.CONFIRM_YES: self.screen.confirmation.confirm, 75 | CustomNavInsID.CONFIRM_NO: self.screen.confirmation.reject, 76 | CustomNavInsID.KEYBOARD_WRITE: self.screen.keyboard.write, 77 | CustomNavInsID.KEYBOARD_TO_CONFIRM: self.screen.keyboard_confirm.tap, 78 | CustomNavInsID.KEYBOARD_TO_MENU: self.screen.keyboard_cancel.tap, 79 | CustomNavInsID.LIST_CHOOSE: self._choose, 80 | CustomNavInsID.CHOOSE_KBL_QWERTY: partial(self._choose, 0), 81 | CustomNavInsID.CHOOSE_KBL_QWERTY_INTL: partial(self._choose, 1), 82 | CustomNavInsID.CHOOSE_KBL_AZERTY: partial(self._choose, 2), 83 | CustomNavInsID.DISCLAIMER_CONFIRM: self.screen.disclaimer.confirm, 84 | CustomNavInsID.DISCLAIMER_REJECT: self.screen.disclaimer.reject, 85 | } 86 | super().__init__(backend, device, callbacks, golden_run) 87 | 88 | def _choose(self, position: int): 89 | # Choosing a field in settings list will display a temporary screen where the chosen field 90 | # is highlighted, then will go the result page. The sleep helps **not** catching this 91 | # intermediate screen 92 | self.screen.list_choice.choose(position) 93 | sleep(0.2) 94 | -------------------------------------------------------------------------------- /tests/functional/stax/screen.py: -------------------------------------------------------------------------------- 1 | from ledgered.devices import Device 2 | from ragger.backend import BackendInterface 3 | from ragger.firmware.touch import MetaScreen 4 | from ragger.firmware.touch.use_cases import UseCaseChoice, UseCaseHomeExt, UseCaseReview, \ 5 | UseCaseSettings 6 | from ragger.firmware.touch.layouts import ChoiceList, FullKeyboardLetters, \ 7 | LeftHeader, TappableCenter 8 | 9 | 10 | class RadioList: 11 | 12 | def __init__(self, backend: BackendInterface, device: Device): 13 | self.backend = backend 14 | self.device = device 15 | 16 | def choose(self, index: int): 17 | positions = [(200, 130), (200, 210), (200, 290), (200, 370), (200, 450)] 18 | assert 0 <= index <= 4, "Choice index must be in [0, 4]" 19 | self.backend.finger_touch(*positions[index]) 20 | 21 | 22 | class CustomStaxScreen(metaclass=MetaScreen): 23 | 24 | use_case_home = UseCaseHomeExt 25 | layout_kbl_choice = ChoiceList 26 | use_case_settings = UseCaseSettings 27 | use_case_menu = UseCaseSettings 28 | use_case_confirmation = UseCaseReview 29 | use_case_disclaimer = UseCaseChoice 30 | layout_menu_choice = ChoiceList 31 | layout_list_choice = RadioList 32 | layout_keyboard = FullKeyboardLetters 33 | layout_keyboard_confirm = TappableCenter 34 | layout_keyboard_cancel = LeftHeader 35 | -------------------------------------------------------------------------------- /tests/functional/stax/test_common.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from requests.exceptions import ConnectionError 3 | 4 | from .navigator import CustomNavInsID 5 | 6 | 7 | @pytest.mark.use_on_device("stax") 8 | def test_immediate_quit(navigator): 9 | instructions = [ 10 | CustomNavInsID.DISCLAIMER_CONFIRM, 11 | CustomNavInsID.CHOOSE_KBL_QWERTY, 12 | CustomNavInsID.HOME_TO_QUIT 13 | ] 14 | with pytest.raises(ConnectionError): 15 | navigator.navigate(instructions, 16 | screen_change_before_first_instruction=False, 17 | screen_change_after_last_instruction=False) 18 | -------------------------------------------------------------------------------- /tests/functional/stax/test_passwords.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ragger.navigator import NavIns, NavInsID 3 | 4 | from .navigator import CustomNavInsID 5 | 6 | 7 | @pytest.mark.use_on_device("stax") 8 | def test_delete_one_password(navigator, functional_test_directory): 9 | instructions = [ 10 | CustomNavInsID.DISCLAIMER_CONFIRM, 11 | CustomNavInsID.CHOOSE_KBL_QWERTY, 12 | CustomNavInsID.HOME_TO_MENU, 13 | # ensure the password list is filled with populated passwords 14 | CustomNavInsID.MENU_TO_DELETE, 15 | # choose the password to delete then confirm 16 | NavIns(CustomNavInsID.LIST_CHOOSE, (2, )), 17 | CustomNavInsID.CONFIRM_YES, 18 | NavIns(NavInsID.WAIT, (2, )), 19 | # check the password has been removed from the list 20 | CustomNavInsID.MENU_TO_DISPLAY, 21 | CustomNavInsID.LIST_TO_MENU 22 | ] 23 | navigator.navigate_and_compare(functional_test_directory, 24 | "delete_one_password", 25 | instructions, 26 | screen_change_before_first_instruction=False) 27 | 28 | 29 | @pytest.mark.use_on_device("stax") 30 | def test_delete_all_passwords(navigator, functional_test_directory): 31 | instructions = [ 32 | CustomNavInsID.DISCLAIMER_CONFIRM, 33 | CustomNavInsID.CHOOSE_KBL_QWERTY, 34 | CustomNavInsID.HOME_TO_MENU, 35 | # ensure the password list is filled with populated passwords 36 | CustomNavInsID.MENU_TO_DELETE_ALL, 37 | # confirm 38 | CustomNavInsID.CONFIRM_YES, 39 | NavIns(NavInsID.WAIT, (2, )), 40 | # check the password has been removed from the list 41 | CustomNavInsID.MENU_TO_DISPLAY, 42 | ] 43 | navigator.navigate_and_compare(functional_test_directory, 44 | "delete_all_password", 45 | instructions, 46 | screen_change_before_first_instruction=False) 47 | 48 | 49 | @pytest.mark.use_on_device("stax") 50 | def test_create_password(navigator, functional_test_directory): 51 | instructions = [ 52 | CustomNavInsID.DISCLAIMER_CONFIRM, 53 | CustomNavInsID.CHOOSE_KBL_QWERTY, 54 | CustomNavInsID.HOME_TO_MENU, 55 | # ensure the password list is filled with populated passwords 56 | CustomNavInsID.MENU_TO_DISPLAY, 57 | CustomNavInsID.LIST_TO_MENU, 58 | # create a new password 59 | CustomNavInsID.MENU_TO_CREATE, 60 | NavIns(CustomNavInsID.KEYBOARD_WRITE, ("n", )), 61 | NavIns(CustomNavInsID.KEYBOARD_WRITE, ("e", )), 62 | NavIns(CustomNavInsID.KEYBOARD_WRITE, ("w", )), 63 | CustomNavInsID.KEYBOARD_TO_CONFIRM, 64 | # return to list to see the newly created password 65 | CustomNavInsID.MENU_TO_DISPLAY, 66 | ] 67 | navigator.navigate_and_compare(functional_test_directory, 68 | "create_password", 69 | instructions, 70 | screen_change_before_first_instruction=False) 71 | -------------------------------------------------------------------------------- /tests/functional/stax/test_settings.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from requests.exceptions import ConnectionError 3 | 4 | from .navigator import CustomNavInsID 5 | 6 | 7 | 8 | @pytest.mark.use_on_device("stax") 9 | def test_settings_screens(navigator, functional_test_directory): 10 | instructions = [ 11 | CustomNavInsID.DISCLAIMER_CONFIRM, 12 | CustomNavInsID.CHOOSE_KBL_QWERTY, 13 | CustomNavInsID.HOME_TO_SETTINGS, 14 | CustomNavInsID.SETTINGS_NEXT, 15 | CustomNavInsID.SETTINGS_NEXT, 16 | CustomNavInsID.SETTINGS_NEXT, 17 | CustomNavInsID.SETTINGS_TO_HOME, 18 | CustomNavInsID.HOME_TO_QUIT 19 | ] 20 | with pytest.raises(ConnectionError): 21 | navigator.navigate_and_compare(functional_test_directory, 22 | "settings", 23 | instructions, 24 | screen_change_before_first_instruction=False, 25 | screen_change_after_last_instruction=False) 26 | -------------------------------------------------------------------------------- /tests/functional/test_cmd.py: -------------------------------------------------------------------------------- 1 | def test_app_info(cmd): 2 | assert cmd.get_app_info() == ("Passwords", "1.2.0") 3 | 4 | 5 | def test_app_config(cmd): 6 | assert cmd.get_app_config() == (4096, 0, 0) 7 | 8 | 9 | def test_generate_password(cmd, test_vector): 10 | charset, seed, expected = test_vector 11 | assert cmd.generate_password(charset, seed) == expected 12 | 13 | 14 | def test_dump_metadatas(cmd, test_vector): 15 | size, expected = test_vector 16 | assert cmd.dump_metadatas(size) == expected 17 | cmd.reset_approval_state() 18 | 19 | 20 | def test_load_metadatas(cmd, test_vector): 21 | # [0] to avoid huge test names filled with the data. 22 | # Instead, it is filled with the data index 23 | metadatas = test_vector[0] 24 | cmd.load_metadatas(metadatas) 25 | assert cmd.dump_metadatas(len(metadatas)) == metadatas 26 | cmd.reset_approval_state() 27 | -------------------------------------------------------------------------------- /tests/functional/test_error_cmd.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from exception import ClaNotSupportedError, InsNotSupportedError, WrongP1P2Error, \ 4 | WrongDataLengthError, MetadatasParsingError, DeviceException 5 | 6 | 7 | @pytest.mark.xfail(raises=ClaNotSupportedError) 8 | def test_bad_cla(cmd): 9 | response = cmd.transport.exchange(cla=0xa0, # 0xa0 instead of 0xe0 10 | ins=0x03, 11 | p1=0x00, 12 | p2=0x00, 13 | data=b"") 14 | 15 | raise DeviceException(error_code=response.status) 16 | 17 | 18 | @pytest.mark.xfail(raises=InsNotSupportedError) 19 | def test_bad_ins(cmd): 20 | response = cmd.transport.exchange(cla=0xe0, 21 | ins=0xAA, # INS 0xAA is not supported 22 | p1=0x00, 23 | p2=0x00, 24 | data=b"") 25 | 26 | raise DeviceException(error_code=response.status) 27 | 28 | 29 | @pytest.mark.xfail(raises=WrongP1P2Error) 30 | def test_wrong_p1p2(cmd): 31 | response = cmd.transport.exchange(cla=0xe0, 32 | ins=0x03, 33 | p1=0x01, # 0x01 instead of 0x00 34 | p2=0x00, 35 | data=b"") 36 | 37 | raise DeviceException(error_code=response.status) 38 | 39 | 40 | @pytest.mark.xfail(raises=WrongDataLengthError) 41 | def test_wrong_data_length(cmd): 42 | # APDUs must be at least 5 bytes: CLA, INS, P1, P2, Lc. 43 | response = cmd.transport.exchange_raw(bytes.fromhex("E000")) 44 | 45 | raise DeviceException(error_code=response.status) 46 | 47 | 48 | @pytest.mark.xfail(raises=WrongDataLengthError) 49 | def test_load_metadatas_with_too_much_data(cmd, test_vector): 50 | # [0] to avoid huge test names filled with the data. 51 | # Instead, it is filled with the data index 52 | metadatas = test_vector[0] 53 | cmd.load_metadatas(metadatas) 54 | 55 | 56 | @pytest.mark.xfail(raises=MetadatasParsingError) 57 | def test_load_metadatas_with_name_too_long(cmd, test_vector): 58 | # [0] to avoid huge test names filled with the data. 59 | # Instead, it is filled with the data index 60 | metadatas = test_vector[0] 61 | cmd.load_metadatas(metadatas) 62 | -------------------------------------------------------------------------------- /tests/functional/tests_vectors.py: -------------------------------------------------------------------------------- 1 | EXISTING_METADATA = b"\n\x00\x07password1\n\x00\x07password2\n\x00\x07password3" 2 | 3 | tests_vectors = { 4 | "test_generate_password": [ 5 | [0x01, "gmail", "HMYDQUIOVKPCKJIHQJEN"], 6 | [0x03, "gmail", "KqIJcPjhENivHvOdmuKQ"], 7 | [0x07, "gmail", "xNX8IQO4vP0ucO41J6JW"], 8 | [0x0F, "gmail", "w14JrbA9HNvWU1ON5MGP"], 9 | [0x1F, "gmail", "vy4Joa86FKvVS1ON4KEP"], 10 | [0x3F, "gmail", "kD83CP1UZO vQvJIuNx4"], 11 | [0x7F, "gmail", "?u8htP1|DO v7vJzYNb4"], 12 | [0xFF, "gmail", "*m8ZlP1|}O vzvJrQNT4"], 13 | [0xFF, "aseedoflengthequal20", "29!uO;UPx UT8Hkmi- 5"], 14 | [0xFF, "aSeedOfLengthEqual20", " $4,P.usI*C\\k1fv2;M;"]], 15 | 16 | "test_dump_metadatas": [ 17 | [0, b""], 18 | [100, EXISTING_METADATA + b"\x00" * (100 - len(EXISTING_METADATA))], 19 | [4096, EXISTING_METADATA + b"\x00" * (4096 - len(EXISTING_METADATA))] 20 | ], 21 | 22 | "test_load_metadatas": [ 23 | # 1-element array to avoid huge test names filled with the data. 24 | # Instead, it is filled with the data index 25 | [b"\x00" * 4096], 26 | [bytes.fromhex("02000761060007616c6c6168")], 27 | [bytes.fromhex("02000761060007616c6c6168") + b"\x00" * (4096 - 12)], 28 | [bytes.fromhex("02000761" "14 00 07 616c6c6168616c6c6168616c6c6168616c6c70") + 29 | b"\x00" * (4096 - 26)], 30 | ], 31 | 32 | "test_load_metadatas_with_too_much_data": [ 33 | # 1-element array to avoid huge test names filled with the data. 34 | # Instead, it is filled with the data index 35 | [b"\x00" * 10000], 36 | [bytes.fromhex("02000761060007616c6c6168") + b"\x00" * 4096], 37 | ], 38 | 39 | "test_load_metadatas_with_name_too_long": [ 40 | # 1-element array to avoid huge test names filled with the data. 41 | # Instead, it is filled with the data index 42 | [bytes.fromhex( 43 | "02000761" "15 00 07 616c6c6168616c6c6168616c6c6168616c6c7078")], 44 | [bytes.fromhex("02000761" "15 00 07 616c6c6168616c6c6168616c6c6168616c6c7078") + 45 | b"\x00" * (4096 - 27)], 46 | ], 47 | } 48 | -------------------------------------------------------------------------------- /tests/unit/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | if(${CMAKE_VERSION} VERSION_LESS 3.10) 4 | cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) 5 | endif() 6 | 7 | # project information 8 | project(unit_tests 9 | VERSION 0.1 10 | DESCRIPTION "Unit tests for the Passwords application" 11 | LANGUAGES C) 12 | 13 | 14 | # guard against bad build-type strings 15 | if (NOT CMAKE_BUILD_TYPE) 16 | set(CMAKE_BUILD_TYPE "Debug") 17 | endif() 18 | 19 | include(CTest) 20 | ENABLE_TESTING() 21 | 22 | # specify C standard 23 | set(CMAKE_C_STANDARD 11) 24 | set(CMAKE_C_STANDARD_REQUIRED True) 25 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -pedantic -g -O0 --coverage") 26 | 27 | set(GCC_COVERAGE_LINK_FLAGS "--coverage -lgcov") 28 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${GCC_COVERAGE_LINK_FLAGS}") 29 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GCC_COVERAGE_LINK_FLAGS}") 30 | 31 | # guard against in-source builds 32 | if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) 33 | message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build \ 34 | directory) and run CMake from there. You may need to remove CMakeCache.txt.") 35 | endif() 36 | 37 | add_compile_definitions(WIDE=) 38 | add_compile_definitions(TEST) 39 | add_compile_definitions(MAX_METANAME=20) 40 | add_compile_definitions(HAVE_BOLOS_UX) 41 | add_compile_definitions(TARGET_STAX) 42 | add_compile_definitions(OS_IO_SEPROXYHAL) 43 | 44 | include_directories(../../src ../../include ./mocks/) 45 | 46 | add_executable(test_stax_password_list stax/test_password_list.c) 47 | add_library(password_list SHARED ../../src/stax/password_list.c) 48 | target_link_libraries(test_stax_password_list PUBLIC cmocka gcov password_list bsd) 49 | 50 | add_executable(test_hid_mapping test_hid_mapping.c) 51 | add_library(hid_mapping SHARED ../../src/hid_mapping.c) 52 | target_link_libraries(test_hid_mapping PUBLIC cmocka gcov hid_mapping bsd) 53 | 54 | add_test(stax_password_list test_stax_password_list) 55 | add_test(hid_mapping test_hid_mapping) 56 | -------------------------------------------------------------------------------- /tests/unit/mocks/os.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define PRINTF printf 9 | #define THROW(X) return 10 | #define EXCEPTION 1 11 | 12 | 13 | bool bolos_ux_mnemonic_check(const unsigned char* buffer, unsigned int length) { 14 | const char* expected_mnemonic = "list of random words which actually are the mnemonic"; 15 | printf("Comparing strings under size '%d'\n", length); 16 | printf(" - expected: '%s'\n", expected_mnemonic); 17 | printf(" - given: '%s'\n", buffer); 18 | return (strncmp(expected_mnemonic, (const char *)buffer, length) == 0); 19 | } 20 | -------------------------------------------------------------------------------- /tests/unit/stax/test_password_list.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "stax/password_list.h" 7 | 8 | 9 | static int setup(void **state __attribute__((unused))) { 10 | // resets the whole buffer with initial values 11 | password_list_reset(); 12 | return 0; 13 | } 14 | 15 | static void test_password_list_reset(void **state __attribute__((unused))) { 16 | for (size_t i = 0; i < DISPLAYED_PASSWORD_PER_PAGE; i++) { 17 | assert_int_equal(password_list_get_offset(i), 0); 18 | assert_null(password_list_get_password(i)); 19 | } 20 | } 21 | 22 | static void test_password_list_get_offset_ok(void **state __attribute__((unused))) { 23 | for (size_t i = 0; i < DISPLAYED_PASSWORD_PER_PAGE - 1; i++) { 24 | assert_int_equal(password_list_get_offset(i), 0); 25 | } 26 | } 27 | 28 | static void test_password_list_get_offset_nok(void **state __attribute__((unused))) { 29 | const size_t index = DISPLAYED_PASSWORD_PER_PAGE; 30 | assert_int_equal(password_list_get_offset(index), -1); 31 | } 32 | 33 | static void test_password_list_add_password(void **state __attribute__((unused))) { 34 | const int index= 2, offset = 7; 35 | const char string[] = "string"; 36 | 37 | assert_null(password_list_get_password(index)); 38 | assert_int_equal(password_list_get_offset(index), 0); 39 | 40 | assert_true(password_list_add_password(index, offset, &string[0], sizeof(string))); 41 | assert_string_equal(password_list_get_password(index), string); 42 | assert_int_equal(password_list_get_offset(index), offset); 43 | } 44 | 45 | static void test_password_list_passwords(void **state __attribute__((unused))) { 46 | const char *strings[] = {"first", "second", "third", "fourth"}; 47 | for (size_t i = 0; i < (sizeof(strings) / sizeof(strings[0])); i++) { 48 | assert_true(password_list_add_password(i, i, strings[i], strlen(strings[i]) + 1)); 49 | } 50 | const char * const *passwords = password_list_passwords(); 51 | for (size_t i = 0; i < (sizeof(strings) / sizeof(strings[0])); i++) { 52 | assert_string_equal(passwords[i], strings[i]); 53 | assert_string_equal(password_list_get_password(i), strings[i]); 54 | } 55 | } 56 | 57 | static void test_password_list_get_password_nok(void **state __attribute__((unused))) { 58 | assert_null(password_list_get_password(DISPLAYED_PASSWORD_PER_PAGE+1)); 59 | } 60 | 61 | static void test_password_list_passwords_nok(void **state __attribute__((unused))) { 62 | size_t i; 63 | for (i = 0; i < DISPLAYED_PASSWORD_PER_PAGE; i++) { 64 | assert_true(password_list_add_password(i, 0, "whatever", 1)); 65 | } 66 | // not enough space 67 | assert_false(password_list_add_password(i, 0, "no more", 1)); 68 | } 69 | 70 | static void test_password_list_reset_buffer(void **state __attribute__((unused))) { 71 | const char *strings[] = {"first", "second", "third", "fourth"}; 72 | for (size_t i = 0; i < (sizeof(strings) / sizeof(strings[0])); i++) { 73 | assert_true(password_list_add_password(i, i, strings[i], strlen(strings[i]) + 1)); 74 | } 75 | password_list_reset_buffer(); 76 | const char * const *passwords = password_list_passwords(); 77 | for (size_t i = 0; i < (sizeof(strings) / sizeof(strings[0])); i++) { 78 | // all passwords have been removed 79 | assert_ptr_equal(*passwords[i], NULL); 80 | } 81 | } 82 | 83 | 84 | static void test_password_list_set_current(void **state __attribute__((unused))) { 85 | const char *strings[] = {"first", "second", "third", "fourth"}; 86 | for (size_t i = 0; i < (sizeof(strings) / sizeof(strings[0])); i++) { 87 | assert_true(password_list_add_password(i, i * 2, strings[i], strlen(strings[i]) + 1)); 88 | } 89 | size_t index = 3; 90 | password_list_set_current(index); 91 | assert_int_equal(password_list_get_current_offset(), index * 2); 92 | } 93 | 94 | int main() { 95 | const struct CMUnitTest tests[] = { 96 | cmocka_unit_test_setup_teardown(test_password_list_reset, setup, NULL), 97 | cmocka_unit_test_setup_teardown(test_password_list_get_offset_ok, setup, NULL), 98 | cmocka_unit_test_setup_teardown(test_password_list_get_offset_nok, setup, NULL), 99 | cmocka_unit_test_setup_teardown(test_password_list_add_password, setup, NULL), 100 | cmocka_unit_test_setup_teardown(test_password_list_passwords, setup, NULL), 101 | cmocka_unit_test_setup_teardown(test_password_list_get_password_nok, setup, NULL), 102 | cmocka_unit_test_setup_teardown(test_password_list_passwords_nok, setup, NULL), 103 | cmocka_unit_test_setup_teardown(test_password_list_reset_buffer, setup, NULL), 104 | cmocka_unit_test_setup_teardown(test_password_list_set_current, setup, NULL), 105 | }; 106 | return cmocka_run_group_tests(tests, NULL, NULL); 107 | } 108 | -------------------------------------------------------------------------------- /tests/unit/test_hid_mapping.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "hid_mapping.h" 7 | 8 | 9 | const char *test = "a&b~c#d {e\"f'g(h -i _j)k=l+m [n |o \\p^q @r ]s }t$u!v:w/x;y.z,A?B D`EFGHIJKLMNOPQRSTUVWXYZ0123456789"; 10 | 11 | 12 | void check_map(uint8_t *map, uint8_t first, uint8_t third) { 13 | assert_int_equal(map[0], first); 14 | assert_int_equal(map[1], 0); 15 | assert_int_equal(map[2], third); 16 | } 17 | 18 | static void test_map_char_qwerty_regular(void **state __attribute__((unused))) { 19 | uint8_t map[3] = {0, 0, 0}; 20 | map_char(HID_MAPPING_QWERTY, 'a', map); 21 | check_map(map, 0, 0x4); 22 | map_char(HID_MAPPING_QWERTY, 'z', map); 23 | check_map(map, 0, 0x1d); 24 | map_char(HID_MAPPING_QWERTY, 'q', map); 25 | check_map(map, 0, 0x14); 26 | map_char(HID_MAPPING_QWERTY, 'w', map); 27 | check_map(map, 0, 0x1a); 28 | map_char(HID_MAPPING_QWERTY, '1', map); 29 | check_map(map, 0, 0x1e); 30 | map_char(HID_MAPPING_QWERTY, '8', map); 31 | check_map(map, 0, 0x25); 32 | map_char(HID_MAPPING_QWERTY, '.', map); 33 | check_map(map, 0, 0x37); 34 | } 35 | 36 | static void test_map_char_qwerty_shift(void **state __attribute__((unused))) { 37 | uint8_t map[3] = {0, 0, 0}; 38 | map_char(HID_MAPPING_QWERTY, 'A', map); 39 | check_map(map, SHIFT_KEY, 0x4); 40 | map_char(HID_MAPPING_QWERTY, 'Z', map); 41 | check_map(map, SHIFT_KEY, 0x1d); 42 | map_char(HID_MAPPING_QWERTY, 'Q', map); 43 | check_map(map, SHIFT_KEY, 0x14); 44 | map_char(HID_MAPPING_QWERTY, 'W', map); 45 | check_map(map, SHIFT_KEY, 0x1a); 46 | map_char(HID_MAPPING_QWERTY, '!', map); 47 | check_map(map, SHIFT_KEY, 0x1e); 48 | map_char(HID_MAPPING_QWERTY, '*', map); 49 | check_map(map, SHIFT_KEY, 0x25); 50 | map_char(HID_MAPPING_QWERTY, '>', map); 51 | check_map(map, SHIFT_KEY, 0x37); 52 | } 53 | 54 | static void test_map_char_azerty_regular(void **state __attribute__((unused))) { 55 | uint8_t map[3] = {0, 0, 0}; 56 | map_char(HID_MAPPING_AZERTY, 'q', map); 57 | check_map(map, 0, 0x4); 58 | map_char(HID_MAPPING_AZERTY, 'w', map); 59 | check_map(map, 0, 0x1d); 60 | map_char(HID_MAPPING_AZERTY, 'a', map); 61 | check_map(map, 0, 0x14); 62 | map_char(HID_MAPPING_AZERTY, 'z', map); 63 | check_map(map, 0, 0x1a); 64 | map_char(HID_MAPPING_AZERTY, '&', map); 65 | check_map(map, 0, 0x1e); 66 | map_char(HID_MAPPING_AZERTY, '_', map); 67 | check_map(map, 0, 0x25); 68 | map_char(HID_MAPPING_AZERTY, ':', map); 69 | check_map(map, 0, 0x37); 70 | } 71 | 72 | static void test_map_char_azerty_shift(void **state __attribute__((unused))) { 73 | uint8_t map[3] = {0, 0, 0}; 74 | map_char(HID_MAPPING_AZERTY, 'Q', map); 75 | check_map(map, SHIFT_KEY, 0x4); 76 | map_char(HID_MAPPING_AZERTY, 'W', map); 77 | check_map(map, SHIFT_KEY, 0x1d); 78 | map_char(HID_MAPPING_AZERTY, 'A', map); 79 | check_map(map, SHIFT_KEY, 0x14); 80 | map_char(HID_MAPPING_AZERTY, 'Z', map); 81 | check_map(map, SHIFT_KEY, 0x1a); 82 | map_char(HID_MAPPING_AZERTY, '1', map); 83 | check_map(map, SHIFT_KEY, 0x1e); 84 | map_char(HID_MAPPING_AZERTY, '8', map); 85 | check_map(map, SHIFT_KEY, 0x25); 86 | map_char(HID_MAPPING_AZERTY, '/', map); 87 | check_map(map, SHIFT_KEY, 0x37); 88 | } 89 | 90 | static void test_map_char_azerty_alt(void **state __attribute__((unused))) { 91 | uint8_t map[3] = {0, 0, 0}; 92 | map_char(HID_MAPPING_AZERTY, '@', map); 93 | check_map(map, ALT_KEY, 0x27); 94 | map_char(HID_MAPPING_AZERTY, '`', map); 95 | check_map(map, ALT_KEY, 0x24); 96 | } 97 | 98 | int main() { 99 | const struct CMUnitTest tests[] = { 100 | cmocka_unit_test_setup_teardown(test_map_char_qwerty_regular, NULL, NULL), 101 | cmocka_unit_test_setup_teardown(test_map_char_qwerty_shift, NULL, NULL), 102 | cmocka_unit_test_setup_teardown(test_map_char_azerty_regular, NULL, NULL), 103 | cmocka_unit_test_setup_teardown(test_map_char_azerty_shift, NULL, NULL), 104 | cmocka_unit_test_setup_teardown(test_map_char_azerty_alt, NULL, NULL), 105 | }; 106 | return cmocka_run_group_tests(tests, NULL, NULL); 107 | } 108 | --------------------------------------------------------------------------------