├── .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 |
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 |

58 |
Passwords Backup
59 |
60 |
61 |
62 | ask_device(passwordsManager)}
68 | />
69 |
75 | ask_device(passwordsManager, () =>
76 | passwordsManager.dump_metadatas()
77 | )
78 | }
79 | />
80 |
86 | ask_device(passwordsManager, () =>
87 | passwordsManager.load_metadatas(metadatas)
88 | )
89 | }
90 | />
91 |
92 |
93 |
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 |
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 |
--------------------------------------------------------------------------------