Migrate from GitHub
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
|
.DS_Store
|
||||||
|
._*
|
||||||
|
.clang-format
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[submodule "lib/Arduino-Base32-Decode"]
|
||||||
|
path = lib/Arduino-Base32-Decode
|
||||||
|
url = https://github.com/dirkx/Arduino-Base32-Decode
|
||||||
|
[submodule "lib/Callback"]
|
||||||
|
path = lib/Callback
|
||||||
|
url = https://github.com/tomstewart89/Callback
|
||||||
|
[submodule "lib/ESP32-BLE-CompositeHID"]
|
||||||
|
path = lib/ESP32-BLE-CompositeHID
|
||||||
|
url = https://github.com/Mystfit/ESP32-BLE-CompositeHID
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
# PWDer
|
||||||
|
PWDer is a simple password manager for the M5Cardputer. Its name is a combination of a few words:
|
||||||
|
- PWD (password)
|
||||||
|
- Manager
|
||||||
|
- Cardputer
|
||||||
|
|
||||||
|
and in order to not break your tongue, you can pronounce it as "Powder".
|
||||||
|
|
||||||
|
<img src="./assets/photos/pwder.bmp" alt="PWDer's splash screen" width="480px">
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<img src="./assets/photos/pwder-device.jpg" alt="PWDer on a Cardputer" width="480px">
|
||||||
|
|
||||||
|
It started as an idea to solve the problem of the meticulous process of logging into personal accounts on different public computers (like school or work), where you don't have your password manager. It simulates a keyboard, entering the passwords for you into every computer you plug it in to. I thought of portable / cloud password managers only when I was halfway through doing this project, so I decided to continue it.
|
||||||
|
# Key functionalities
|
||||||
|
- Storing up to 100 login + password + otp entries using AES128 encryption
|
||||||
|
- Protection with master password (PBKDF2 key derivation)
|
||||||
|
- Searching for passwords by entry name
|
||||||
|
- Automatic password input to your computer using USB cable or Bluetooth
|
||||||
|
- Synchronizing with .kdbx (KeePass) files over USB cable
|
||||||
|
- Support for TOTP two-factor authentication
|
||||||
|
- Fun, simple design
|
||||||
|
|
||||||
|
# Limitations
|
||||||
|
- Currently, only ASCII characters are supported
|
||||||
|
- Simultaneous use of Wi-Fi and Bluetooth is impossible. If you want to use 2FA with Bluetooth, please use M5Unit RTC.
|
||||||
|
|
||||||
|
# Setting it up
|
||||||
|
## Prerequisites
|
||||||
|
- I highly recommend using [Launcher](https://github.com/bmorcelli/Launcher) for running the program. However, it can be flashed and used directly too.
|
||||||
|
- A .kdbx database is required to import passwords into the Cardputer. Your computer also has to be able to run Python scripts. (This requirement will disappear once I add manual password entering)
|
||||||
|
## Step 1: Preparing the SD card
|
||||||
|
- Format an SD card using the **FAT32** file system
|
||||||
|
## Step 2a: Installing PWDer on the Cardputer (binary)
|
||||||
|
- Download the binary from the Releases page and put it on your SD card or upload it directly to the ESP32.
|
||||||
|
## Step 2b: Building PWDer from source
|
||||||
|
- Set up [PlatformIO](https://platformio.org/install).
|
||||||
|
- Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/adamecki/PWDer
|
||||||
|
cd PWDer
|
||||||
|
git submodule init
|
||||||
|
```
|
||||||
|
|
||||||
|
- Open cloned directory in PlatformIO. The IDE should pull required libraries automatically.
|
||||||
|
- Build the program and either upload it to the device or find its binary (`PROJECT_DIR/pio/build/m5stack-stamps3/firmware.bin`).
|
||||||
|
- *Optional: If you chose the second option, move the exported .bin file to the SD card*
|
||||||
|
- Make sure the SD card is in, then turn on the Cardputer
|
||||||
|
## Step 3: First run
|
||||||
|
- If you're using launcher, install the .bin file. Otherwise, wait until the program asks for the password.
|
||||||
|
- The first run password is "default". Enter it and press OK, you'll see the "Sample Entry" screen - the default and only password entry if the passwords file was nonexistent.
|
||||||
|
## Step 4: Importing the passwords for the first time
|
||||||
|
- Prepare your .kdbx database
|
||||||
|
- Open your terminal in `pwder_keepass_sync` directory
|
||||||
|
- Create a Python virtual environment and install dependencies
|
||||||
|
|
||||||
|
macOS / Linux:
|
||||||
|
```bash
|
||||||
|
python3 -m venv .
|
||||||
|
source ./bin/activate
|
||||||
|
pip install cryptography pykeepass pyserial
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
```cmd
|
||||||
|
python -m venv .
|
||||||
|
Scripts\activate.bat
|
||||||
|
pip install cryptography pykeepass pyserial
|
||||||
|
```
|
||||||
|
|
||||||
|
(next time you try to use the script, just enter `source ./bin/activate` or `Scripts\activate.bat`)
|
||||||
|
- Connect the Cardputer to your computer using the USB cable (make sure PWDer is running and unlocked).
|
||||||
|
- Run the script
|
||||||
|
|
||||||
|
macOS / Linux:
|
||||||
|
```bash
|
||||||
|
python3 sync.py /your/database.kdbx
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
```cmd
|
||||||
|
python sync.py C:\your\database.kdbx
|
||||||
|
```
|
||||||
|
|
||||||
|
<img src="./assets/photos/pyscript.webp" alt="Password synchronization script" width="480px">
|
||||||
|
|
||||||
|
- The script will prompt you for both your KDBX password and PWDer password. Enter them and then press Y on the Cardputer to import your vault.
|
||||||
|
|
||||||
|
<img src="./assets/photos/import.jpg" alt="Passwords found" width="480px">
|
||||||
|
|
||||||
|
<b>Remember that your user should have the serial port privileges (like belonging to the `dialout` group on Linux)!</b>
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
## Main screen
|
||||||
|
This is where you can select a password to enter. The top bar represents the vault usage in %, and the bottom one is a scrollbar.
|
||||||
|
|
||||||
|
<img src="./assets/photos/no-otp.bmp" alt="main" width="480px">
|
||||||
|
|
||||||
|
|
||||||
|
Press the arrow keys to navigate, and hold down V to preview the password / OTP you're about to enter. Other keybinds:
|
||||||
|
|
||||||
|
- `1` - enter the username for currently selected entry
|
||||||
|
- `2` - enter the password
|
||||||
|
- `3` - enter the username, then press TAB, then enter the password, and then press Enter
|
||||||
|
- `4` - enter current TOTP
|
||||||
|
- `Enter` - perform one of these three actions, depending on the selection made in Options.
|
||||||
|
|
||||||
|
- `T` - simulate pressing TAB on the computer.
|
||||||
|
- `R` - simulate pressing Enter on the computer.
|
||||||
|
|
||||||
|
- `M` - mute or unmute the speaker. The default state for the speaker is muted.
|
||||||
|
- `L` - lock the device. Alternatively, you can just reset the device.
|
||||||
|
|
||||||
|
- `N` - connect (or reconnect) Wi-Fi
|
||||||
|
- `B` - enable Bluetooth connectivity (only one of Wi-Fi / Bluetooth can work at the same time)
|
||||||
|
|
||||||
|
- `O` - open Options.
|
||||||
|
- `C` - open About page.
|
||||||
|
- `Q` - open Search menu (press Fn+Esc to exit)
|
||||||
|
|
||||||
|
- `FN` + `Top button` - screenshot (will be saved as BMP in the SD card root directory)
|
||||||
|
|
||||||
|
If navigating with Arrow keys / Esc doesn't work, press FN alongside them.
|
||||||
|
|
||||||
|
## Bluetooth
|
||||||
|
### Introduction
|
||||||
|
PWDer can simulate a Bluetooth keyboard to input passwords to your computer without a cable.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
Press B on Cardputer's main screen. The device should instantly become visible to a computer.
|
||||||
|
|
||||||
|
<img src="./assets/photos/bluetooth-computer.png" alt="Bluetooth 1" width="480px">
|
||||||
|
|
||||||
|
Connect, and PWDer should display a Bluetooth icon in the top right corner.
|
||||||
|
|
||||||
|
<img src="./assets/photos/bluetooth.bmp" alt="Bluetooth 2" width="480px">
|
||||||
|
|
||||||
|
Remember that currently using Wi-Fi and Bluetooth simultaneously in PWDer is currently impossible. If you encounter an error enabling Wi-Fi or Bluetooth, restart PWDer and enable only the connectivity you intend to use at the time.
|
||||||
|
|
||||||
|
### Connecting to a new device
|
||||||
|
Forget the device on any connected host and restart PWDer, enabling Bluetooth. It should be visible in the devices list again.
|
||||||
|
|
||||||
|
### Disclaimer
|
||||||
|
Keep in mind that every wireless connection is captured with much more ease than a wired one. Use Bluetooth only in safe environments, and at your own risk!
|
||||||
|
|
||||||
|
## TOTP
|
||||||
|
### Introduction
|
||||||
|
PWDer supports two-factor authentication using six-digit one time password.
|
||||||
|
|
||||||
|
Neither ESP32, nor Cardputer do have an RTC backup battery to keep the time correct even when the device is powered off. Currently, there are two options to use time-based one-time passwords with PWDer.
|
||||||
|
|
||||||
|
### First solution: M5Unit RTC
|
||||||
|
If you have an M5Unit RTC, connect it to Cardputer's Grove connector before starting PWDer. It will detect the unit and display a small clock icon in the top right corner. It means that Cardputer is using RTC for generating TOTP.
|
||||||
|
|
||||||
|
<img src="./assets/photos/otp.bmp" alt="TOTP is here" width="480px">
|
||||||
|
|
||||||
|
<img src="./assets/photos/m5unitrtc.webp" alt="M5Unit RTC attached to a Cardputer" width="480px">
|
||||||
|
|
||||||
|
If your RTC clock is not set correctly, you can synchronize its time with network in the Options (look below). In the future, setting time manually will also be possible.
|
||||||
|
|
||||||
|
*Notice: Don't connect any other units to the Grove connector while PWDer is running. It hasn't been tested, but Cardputer might read it incorrectly as an M5Unit RTC and give incorrect time-based one-time passwords.
|
||||||
|
|
||||||
|
### Second solution: NTP network time
|
||||||
|
The Cardputer can to connect to an NTP server via Wi-Fi in order to synchronize time, so it can generate a TOTP. For now, the default pool.ntp.org NTP server for the NTP client library is used, but in the future there will be a way to change the NTP server directly from PWDer's UI. For now the only way to change an NTP server is editing the code.
|
||||||
|
|
||||||
|
With that said, even if you have a TOTP secret in your database, PWDer won't show the TOTP if it doesn't have a time provider. If both RTC and NTP are available, Cardputer uses RTC as the primary time provider, as it is more reliable (works offline).
|
||||||
|
|
||||||
|
### How to add a TOTP secret to my entry?
|
||||||
|
<img src="./assets/photos/kptotp.webp" alt="KeePass TOTP settings for an entry" width="480px">
|
||||||
|
KeePassXC supports TOTP. If a secret key for your entry is set, the Python vault extraction script provided in the `pwder_keepass_sync` directory will find it and place it in your import file for PWDer. Just like passwords, it is stored on the SD card in an encrypted form (excluding the import phase for now).
|
||||||
|
|
||||||
|
Otherwise, if you're writing an import file manually, you can add the secret key as the fourth line of each entry, as described earlier.
|
||||||
|
|
||||||
|
### Where do I find the TOTP in PWDer's UI?
|
||||||
|
<img src="./assets/photos/preview.bmp" alt="TOTP is here" width="480px">
|
||||||
|
If the NTP requirements are met and your entry has a TOTP secret set, the entry title will be blue. After that, you can hold `v` to display the one time password: it will show on the right side of your username, or press `4` to enter it to your computer.
|
||||||
|
|
||||||
|
## Lock screen
|
||||||
|
Here you have to enter the correct password (then press OK) to access the device. If you've locked yourself out, you can remove the `pwder/config` file from the SD card (the password will be "default" again), but keep in mind that all your saved passwords and configuration will disappear!
|
||||||
|
|
||||||
|
<img src="./assets/photos/password.bmp" alt="lock" width="480px">
|
||||||
|
|
||||||
|
## Handbook
|
||||||
|
The handbook is a quick guide to all the keybindings. Press the arrow keys / esc to navigate.
|
||||||
|
|
||||||
|
<img src="./assets/photos/handbook.bmp" alt="handbook" width="480px">
|
||||||
|
|
||||||
|
## Options
|
||||||
|
Press O to open the options.
|
||||||
|
|
||||||
|
<img src="./assets/photos/opts1.bmp" alt="opts1" width="480px">
|
||||||
|
|
||||||
|
Press FN + Arrow keys to change the brightness. Remember that you need to apply this setting with Enter!
|
||||||
|
|
||||||
|
<img src="./assets/photos/opts2.bmp" alt="opts2" width="480px">
|
||||||
|
|
||||||
|
Press Enter to switch between default credential input modes.
|
||||||
|
|
||||||
|
<img src="./assets/photos/opts3.bmp" alt="opts3" width="480px">
|
||||||
|
|
||||||
|
Press Enter to switch the inactivity lock on or off. The screen will dim after 15s, go blank after 20s and lock itself after 30s of inactivity.
|
||||||
|
|
||||||
|
<img src="./assets/photos/opts4.bmp" alt="opts4" width="480px">
|
||||||
|
|
||||||
|
Press Enter to switch color schemes. Look below to see the available ones.
|
||||||
|
|
||||||
|
<img src="./assets/photos/opts5.bmp" alt="opts5" width="480px">
|
||||||
|
|
||||||
|
Here you can change the device's master password. Type it, press Enter, then enter the old password.
|
||||||
|
|
||||||
|
<img src="./assets/photos/opts6.bmp" alt="opts6" width="480px">
|
||||||
|
|
||||||
|
Here you can synchronize the time with NTP for the M5Unit RTC. Notice that the time shown is UTC, not local time!
|
||||||
|
|
||||||
|
<img src="./assets/photos/opts7.bmp" alt="opts7" width="480px">
|
||||||
|
|
||||||
|
Wi-Fi SSID for NTP connection.
|
||||||
|
|
||||||
|
<img src="./assets/photos/opts8.bmp" alt="opts8" width="480px">
|
||||||
|
|
||||||
|
Wi-Fi password for NTP connection.
|
||||||
|
|
||||||
|
## About page
|
||||||
|
Press the arrow keys / esc to navigate. Pressing Enter will input the link to the selected thing (preferably to the browser).
|
||||||
|
|
||||||
|
<img src="./assets/photos/about.bmp" alt="about" width="480px">
|
||||||
|
|
||||||
|
## Repaint PWDer!
|
||||||
|
Seven default color schemes are available. File `include/gui.h` contains their definitions.
|
||||||
|
|
||||||
|
<img src="./assets/photos/theme1.bmp" alt="Classic" width="480px">
|
||||||
|
|
||||||
|
Classic
|
||||||
|
|
||||||
|
<img src="./assets/photos/theme2.bmp" alt="Classic Dark" width="480px">
|
||||||
|
|
||||||
|
Classic Dark
|
||||||
|
|
||||||
|
<img src="./assets/photos/theme3.bmp" alt="Industrial" width="480px">
|
||||||
|
|
||||||
|
Industrial
|
||||||
|
|
||||||
|
<img src="./assets/photos/theme4.bmp" alt="Sky" width="480px">
|
||||||
|
|
||||||
|
Sky
|
||||||
|
|
||||||
|
<img src="./assets/photos/theme5.bmp" alt="Garden" width="480px">
|
||||||
|
|
||||||
|
Garden
|
||||||
|
|
||||||
|
<img src="./assets/photos/theme6.bmp" alt="Elegant" width="480px">
|
||||||
|
|
||||||
|
Elegant
|
||||||
|
|
||||||
|
<img src="./assets/photos/theme7.bmp" alt="L33t H4xx0r" width="480px">
|
||||||
|
|
||||||
|
L33t H4xx0r
|
||||||
|
|
||||||
|
# Other languages
|
||||||
|
- Polish is available. To change a language, recompile PWDer with `#define lang_pl` instead of default `#define lang_en` in line 23 of the file `include/globals.h`.
|
||||||
|
|
||||||
|
# Roadmap
|
||||||
|
In future versions, I plan to include these features:
|
||||||
|
- Manual password adding
|
||||||
|
- Wireless password synchronization via Wi-Fi
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# PWDer icon drawing mechanism
|
||||||
|
PWDer stores its icons in program memory under a pwder_style::icon32 struct (see `/include/icons.h`).
|
||||||
|
The struct contains following data:
|
||||||
|
|
||||||
|
## .bitmap[3072] (uint8_t)
|
||||||
|
This is a byte array containing RGB data of the icon, pixel by pixel.
|
||||||
|
`32 x 32` canvas * `3` bytes for each color = `3072` bytes.
|
||||||
|
|
||||||
|
## .mask[1024]
|
||||||
|
This is a bool array containing a mask, telling the push_icon function whether the pixel should be drawn or not.
|
||||||
|
It is useful when a transparent background has to be drawn.
|
||||||
|
|
||||||
|
* If the value is set to `true`, the pixel will be masked (will not be drawn).
|
||||||
|
* If the value is set to `false`, the pixel will not be masked (will be drawn).
|
||||||
|
|
||||||
|
## Using your own icons
|
||||||
|
The requirement is that the icon size is `32 x 32` pixels.
|
||||||
|
|
||||||
|
1. Using your favorite raster graphic editing program, create a 24-bit `32 x 32` BMP file: the colored icon that will be displayed.
|
||||||
|
|
||||||
|
2. Create another, monochrome `32 x 32` BMP file: the mask that will hide the background or anything that should be transparent.
|
||||||
|
* Fill masked area with white pixels and unmasked area with black pixels.
|
||||||
|
|
||||||
|
3. Write a script to convert the RGB BMP to C++ array:
|
||||||
|
* Structure:
|
||||||
|
```cpp
|
||||||
|
/* Red byte */, /* Green byte */, /* Blue byte */, /* Red byte */, /* Green byte */, /* ... */
|
||||||
|
```
|
||||||
|
* Example:
|
||||||
|
```cpp
|
||||||
|
0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, /* ... */
|
||||||
|
```
|
||||||
|
4. Convert the mask BMP to C++ array, converting black pixels to `false` and white ones to `true`:
|
||||||
|
* Example:
|
||||||
|
```cpp
|
||||||
|
true, true, false, false, false, true, false, /* ... */
|
||||||
|
```
|
||||||
|
|
||||||
|
*(I will include the scripts here as well soon)*
|
||||||
|
|
||||||
|
5. Replace data in `icons.cpp`
|
||||||
|
```cpp
|
||||||
|
const pwder_style::icon32 icon_name PROGMEM = {
|
||||||
|
.bitmap = {
|
||||||
|
0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, /* ... */
|
||||||
|
},
|
||||||
|
.mask = {
|
||||||
|
true, true, false, false, false, true, false, /* ... */
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Recompile, and now you have a custom icon.
|
||||||
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 678 B |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 670 B |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 702 B |
|
After Width: | Height: | Size: 674 B |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 678 B |
|
After Width: | Height: | Size: 686 B |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 678 B |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 674 B |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 702 B |
|
After Width: | Height: | Size: 674 B |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 678 B |
|
After Width: | Height: | Size: 686 B |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 96 KiB |
@@ -0,0 +1,112 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <KeyboardHIDCodes.h>
|
||||||
|
|
||||||
|
struct keymap {
|
||||||
|
uint8_t usage_id;
|
||||||
|
bool shift;
|
||||||
|
};
|
||||||
|
|
||||||
|
const keymap ascii_to_hid[128] = {
|
||||||
|
{0, false}, // 00 NUL
|
||||||
|
{0, false}, // 01 SOH
|
||||||
|
{0, false}, // 02 STX
|
||||||
|
{0, false}, // 03 ETX
|
||||||
|
{0, false}, // 04 EOT
|
||||||
|
{0, false}, // 05 ENQ
|
||||||
|
{0, false}, // 06 ACK
|
||||||
|
{0, false}, // 07 BEL
|
||||||
|
{KEY_BACKSPACE, false},// 08 BS (Backspace)
|
||||||
|
{KEY_TAB, false}, // 09 TAB (Tab)
|
||||||
|
{KEY_ENTER, false}, // 10 LF (Line Feed / Enter)
|
||||||
|
{0, false}, // 11 VT
|
||||||
|
{0, false}, // 12 FF
|
||||||
|
{KEY_ENTER, false}, // 13 CR (Carriage Return)
|
||||||
|
{0, false}, // 14 SO
|
||||||
|
{0, false}, // 15 SI
|
||||||
|
{0, false}, // 16 DLE
|
||||||
|
{0, false}, // 17 DC1
|
||||||
|
{0, false}, // 18 DC2
|
||||||
|
{0, false}, // 19 DC3
|
||||||
|
{0, false}, // 20 DC4
|
||||||
|
{0, false}, // 21 NAK
|
||||||
|
{0, false}, // 22 SYN
|
||||||
|
{0, false}, // 23 ETB
|
||||||
|
{0, false}, // 24 CAN
|
||||||
|
{0, false}, // 25 EM
|
||||||
|
{0, false}, // 26 SUB
|
||||||
|
{KEY_ESC, false}, // 27 ESC (Escape)
|
||||||
|
{0, false}, // 28 FS
|
||||||
|
{0, false}, // 29 GS
|
||||||
|
{0, false}, // 30 RS
|
||||||
|
{0, false}, // 31 US
|
||||||
|
|
||||||
|
// Printable Characters 32-64
|
||||||
|
{KEY_SPACE, false}, // 32 Space
|
||||||
|
{KEY_1, true}, // 33 !
|
||||||
|
{KEY_APOSTROPHE, true}, // 34 "
|
||||||
|
{KEY_3, true}, // 35 #
|
||||||
|
{KEY_4, true}, // 36 $
|
||||||
|
{KEY_5, true}, // 37 %
|
||||||
|
{KEY_7, true}, // 38 &
|
||||||
|
{KEY_APOSTROPHE, false}, // 39 '
|
||||||
|
{KEY_9, true}, // 40 (
|
||||||
|
{KEY_0, true}, // 41 )
|
||||||
|
{KEY_8, true}, // 42 *
|
||||||
|
{KEY_EQUAL, true}, // 43 +
|
||||||
|
{KEY_COMMA, false}, // 44 ,
|
||||||
|
{KEY_MINUS, false}, // 45 -
|
||||||
|
{KEY_DOT, false}, // 46 .
|
||||||
|
{KEY_SLASH, false}, // 47 /
|
||||||
|
{KEY_0, false}, // 48 0
|
||||||
|
{KEY_1, false}, // 49 1
|
||||||
|
{KEY_2, false}, // 50 2
|
||||||
|
{KEY_3, false}, // 51 3
|
||||||
|
{KEY_4, false}, // 52 4
|
||||||
|
{KEY_5, false}, // 53 5
|
||||||
|
{KEY_6, false}, // 54 6
|
||||||
|
{KEY_7, false}, // 55 7
|
||||||
|
{KEY_8, false}, // 56 8
|
||||||
|
{KEY_9, false}, // 57 9
|
||||||
|
{KEY_SEMICOLON, true}, // 58 :
|
||||||
|
{KEY_SEMICOLON, false}, // 59 ;
|
||||||
|
{KEY_COMMA, true}, // 60 <
|
||||||
|
{KEY_EQUAL, false}, // 61 =
|
||||||
|
{KEY_DOT, true}, // 62 >
|
||||||
|
{KEY_SLASH, true}, // 63 ?
|
||||||
|
{KEY_2, true}, // 64 @
|
||||||
|
|
||||||
|
// Uppercase Letters 65-90
|
||||||
|
{KEY_A, true}, {KEY_B, true}, {KEY_C, true}, {KEY_D, true},
|
||||||
|
{KEY_E, true}, {KEY_F, true}, {KEY_G, true}, {KEY_H, true},
|
||||||
|
{KEY_I, true}, {KEY_J, true}, {KEY_K, true}, {KEY_L, true},
|
||||||
|
{KEY_M, true}, {KEY_N, true}, {KEY_O, true}, {KEY_P, true},
|
||||||
|
{KEY_Q, true}, {KEY_R, true}, {KEY_S, true}, {KEY_T, true},
|
||||||
|
{KEY_U, true}, {KEY_V, true}, {KEY_W, true}, {KEY_X, true},
|
||||||
|
{KEY_Y, true}, {KEY_Z, true},
|
||||||
|
|
||||||
|
// Symbols 91-96
|
||||||
|
{KEY_LEFTBRACE, false}, // 91 [
|
||||||
|
{KEY_BACKSLASH, false}, // 92 \ //
|
||||||
|
{KEY_RIGHTBRACE, false}, // 93 ]
|
||||||
|
{KEY_6, true}, // 94 ^
|
||||||
|
{KEY_MINUS, true}, // 95 _
|
||||||
|
{KEY_GRAVE, false}, // 96 `
|
||||||
|
|
||||||
|
// Lowercase Letters 97-122
|
||||||
|
{KEY_A, false}, {KEY_B, false}, {KEY_C, false}, {KEY_D, false},
|
||||||
|
{KEY_E, false}, {KEY_F, false}, {KEY_G, false}, {KEY_H, false},
|
||||||
|
{KEY_I, false}, {KEY_J, false}, {KEY_K, false}, {KEY_L, false},
|
||||||
|
{KEY_M, false}, {KEY_N, false}, {KEY_O, false}, {KEY_P, false},
|
||||||
|
{KEY_Q, false}, {KEY_R, false}, {KEY_S, false}, {KEY_T, false},
|
||||||
|
{KEY_U, false}, {KEY_V, false}, {KEY_W, false}, {KEY_X, false},
|
||||||
|
{KEY_Y, false}, {KEY_Z, false},
|
||||||
|
|
||||||
|
// Symbols 123-127
|
||||||
|
{KEY_LEFTBRACE, true}, // 123 {
|
||||||
|
{KEY_BACKSLASH, true}, // 124 |
|
||||||
|
{KEY_RIGHTBRACE, true}, // 125 }
|
||||||
|
{KEY_GRAVE, true}, // 126 ~
|
||||||
|
{KEY_DELETE, false} // 127 DEL
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
void init_new_vault();
|
||||||
|
void export_vault();
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Device libraries
|
||||||
|
#include <M5Cardputer.h>
|
||||||
|
#include <M5GFX.h>
|
||||||
|
#include <SD.h>
|
||||||
|
#include <SPI.h>
|
||||||
|
#include <USB.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
// Software libraries
|
||||||
|
#include "TOTP.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Base32-Decode.h>
|
||||||
|
#include <NTPClient.h>
|
||||||
|
#include <Unit_RTC.h>
|
||||||
|
#include <pvault.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
// Language
|
||||||
|
#define lang_en
|
||||||
|
// #define lang_pl
|
||||||
|
#include "ui_strings.h"
|
||||||
|
|
||||||
|
// SD support definitions
|
||||||
|
#define SD_CS_PIN 12
|
||||||
|
#define SD_MOSI_PIN 14
|
||||||
|
#define SD_MISO_PIN 39
|
||||||
|
#define SD_CLK_PIN 40
|
||||||
|
|
||||||
|
// File paths
|
||||||
|
#define VAULT_PATH "/pwder.pvault"
|
||||||
|
#define IMPORT_FILE_PATH "/pwimport"
|
||||||
|
#define EXPORT_FILE_PATH "/pwexport"
|
||||||
|
|
||||||
|
// default configuration values
|
||||||
|
#define DEFAULT_PASSWORD "default"
|
||||||
|
#define DEFAULT_SSID "sample"
|
||||||
|
#define DEFAULT_WIFI_PASSWORD "password"
|
||||||
|
#define DEFAULT_SYNCHOST "192.168.1.100"
|
||||||
|
#define DEFAULT_SYNCPORT "7305"
|
||||||
|
|
||||||
|
// UI definitions
|
||||||
|
#define UI_UPDATE_MILISECONDS 3000
|
||||||
|
#define MODE3_PAGES_NUMBER 8
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <M5GFX.h>
|
||||||
|
|
||||||
|
namespace pwder_style {
|
||||||
|
struct color_scheme {
|
||||||
|
String name;
|
||||||
|
|
||||||
|
int background_color;
|
||||||
|
int foreground_color;
|
||||||
|
|
||||||
|
int border_color;
|
||||||
|
int statusbar_color;
|
||||||
|
|
||||||
|
int background_text_color;
|
||||||
|
int foreground_text_color;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct icon32 {
|
||||||
|
const uint8_t bitmap[3072];
|
||||||
|
const bool mask[1024];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// spoiler alert
|
||||||
|
const String motd[32] PROGMEM = {
|
||||||
|
"Papa Whiskey Delta echo romeo",
|
||||||
|
"Don't give 'em the SSH keys!",
|
||||||
|
"username: admin, password: admin",
|
||||||
|
"Can a Cardputer run Forza Horizon?",
|
||||||
|
"Pow-There!",
|
||||||
|
"2+2=5, trust me bro",
|
||||||
|
"Corny sunglasses, not gonna lie",
|
||||||
|
"100% NO AI",
|
||||||
|
"What is the definition of insanity?",
|
||||||
|
"Click here to add text",
|
||||||
|
"Password must be 8.5 characters long",
|
||||||
|
"I'm sorry if it crashed",
|
||||||
|
"sudo touch grass",
|
||||||
|
"Now works with smart fridges",
|
||||||
|
"PWDer Holy-C port, anyone?",
|
||||||
|
"Manager, yet all passwords the same",
|
||||||
|
"At least one person uses it",
|
||||||
|
"I dare you to beatbox right now",
|
||||||
|
"You picked the wrong Cardputer, fool",
|
||||||
|
"encrypted = encrypt(decrypted);",
|
||||||
|
"Hello Reddit!",
|
||||||
|
"This line of text is called MOTD",
|
||||||
|
"Make sure nobody's looking!",
|
||||||
|
"Linux users when viewing file: ^._.^",
|
||||||
|
"sudo rm -rf /",
|
||||||
|
"PWDer in Germany: Der PWDer",
|
||||||
|
"Don't lose it!",
|
||||||
|
"ssh $USER@localhost",
|
||||||
|
"I got a card, I got a puter...",
|
||||||
|
"Also try geo-tp's Password Manager",
|
||||||
|
"This is not Windows 95",
|
||||||
|
"Probably doesn't beat Touch ID",
|
||||||
|
};
|
||||||
|
|
||||||
|
const int color_schemes_number = 7;
|
||||||
|
const pwder_style::color_scheme pwcolors[7] PROGMEM = {
|
||||||
|
{
|
||||||
|
.name = "Classic",
|
||||||
|
|
||||||
|
.background_color = NAVY,
|
||||||
|
.foreground_color = LIGHTGREY,
|
||||||
|
|
||||||
|
.border_color = DARKGREY,
|
||||||
|
.statusbar_color = PURPLE,
|
||||||
|
|
||||||
|
.background_text_color = WHITE,
|
||||||
|
.foreground_text_color = BLACK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "Classic Dark",
|
||||||
|
|
||||||
|
.background_color = NAVY,
|
||||||
|
.foreground_color = DARKGREY,
|
||||||
|
|
||||||
|
.border_color = LIGHTGREY,
|
||||||
|
.statusbar_color = PURPLE,
|
||||||
|
|
||||||
|
.background_text_color = WHITE,
|
||||||
|
.foreground_text_color = WHITE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "Industrial",
|
||||||
|
|
||||||
|
.background_color = LIGHTGREY,
|
||||||
|
.foreground_color = WHITE,
|
||||||
|
|
||||||
|
.border_color = DARKGREY,
|
||||||
|
.statusbar_color = BLACK,
|
||||||
|
|
||||||
|
.background_text_color = BLACK,
|
||||||
|
.foreground_text_color = BLACK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "Sky",
|
||||||
|
|
||||||
|
.background_color = SKYBLUE,
|
||||||
|
.foreground_color = WHITE,
|
||||||
|
|
||||||
|
.border_color = LIGHTGREY,
|
||||||
|
.statusbar_color = YELLOW,
|
||||||
|
|
||||||
|
.background_text_color = BLACK,
|
||||||
|
.foreground_text_color = BLACK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "Garden",
|
||||||
|
|
||||||
|
.background_color = GREEN,
|
||||||
|
.foreground_color = WHITE,
|
||||||
|
|
||||||
|
.border_color = LIGHTGREY,
|
||||||
|
.statusbar_color = SKYBLUE,
|
||||||
|
|
||||||
|
.background_text_color = BLACK,
|
||||||
|
.foreground_text_color = BLACK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "Elegance",
|
||||||
|
|
||||||
|
.background_color = PURPLE,
|
||||||
|
.foreground_color = LIGHTGREY,
|
||||||
|
|
||||||
|
.border_color = WHITE,
|
||||||
|
.statusbar_color = DARKGREY,
|
||||||
|
|
||||||
|
.background_text_color = WHITE,
|
||||||
|
.foreground_text_color = BLACK
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.name = "L33t H4xx0r",
|
||||||
|
|
||||||
|
.background_color = BLACK,
|
||||||
|
.foreground_color = BLACK,
|
||||||
|
|
||||||
|
.border_color = DARKGREEN,
|
||||||
|
.statusbar_color = GREEN,
|
||||||
|
|
||||||
|
.background_text_color = GREEN,
|
||||||
|
.foreground_text_color = GREEN
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct bmp_header {
|
||||||
|
// bmp file
|
||||||
|
uint16_t bf_type = 0x4D42;
|
||||||
|
uint32_t bf_size;
|
||||||
|
|
||||||
|
uint16_t bf_reserved_1 = 0;
|
||||||
|
uint16_t bf_reserved_2 = 0;
|
||||||
|
|
||||||
|
uint32_t bf_off_bits = 54;
|
||||||
|
|
||||||
|
// bmp info
|
||||||
|
uint32_t bi_size = 40;
|
||||||
|
|
||||||
|
int32_t bi_width;
|
||||||
|
int32_t bi_height;
|
||||||
|
|
||||||
|
uint16_t bi_planes = 1;
|
||||||
|
uint16_t bi_bit_count = 24;
|
||||||
|
uint32_t bi_compression = 0;
|
||||||
|
uint32_t bi_size_image = 0;
|
||||||
|
|
||||||
|
int32_t bi_x_pels_per_meter = 0;
|
||||||
|
int32_t bi_y_pels_per_meter = 0;
|
||||||
|
|
||||||
|
uint32_t bi_clr_used = 0;
|
||||||
|
uint32_t bi_clr_important = 0;
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
void push_icon(const pwder_style::icon32 &icon, int xoffset, int yoffset, int scale);
|
||||||
|
void no_sdcard_crash_screen();
|
||||||
|
void password_import_screen();
|
||||||
|
void save_screenshot_bmp();
|
||||||
|
void connection_init_error();
|
||||||
|
void splash_screen();
|
||||||
|
void splash_screen_create_progressbar();
|
||||||
|
void splash_screen_update_progressbar_percentage(int progress);
|
||||||
|
void draw_ui();
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "gui.h"
|
||||||
|
|
||||||
|
extern const pwder_style::icon32 loudspeaker_unmuted PROGMEM;
|
||||||
|
extern const pwder_style::icon32 loudspeaker PROGMEM;
|
||||||
|
extern const pwder_style::icon32 clockicon PROGMEM;
|
||||||
|
extern const pwder_style::icon32 bluetooth PROGMEM;
|
||||||
|
extern const pwder_style::icon32 battery PROGMEM;
|
||||||
|
extern const pwder_style::icon32 options PROGMEM;
|
||||||
|
extern const pwder_style::icon32 padlock PROGMEM;
|
||||||
|
extern const pwder_style::icon32 network PROGMEM;
|
||||||
|
extern const pwder_style::icon32 search PROGMEM;
|
||||||
|
extern const pwder_style::icon32 error PROGMEM;
|
||||||
|
extern const pwder_style::icon32 help PROGMEM;
|
||||||
|
extern const pwder_style::icon32 drum PROGMEM;
|
||||||
|
extern const pwder_style::icon32 key PROGMEM;
|
||||||
|
extern const pwder_style::icon32 me PROGMEM;
|
||||||
|
extern const pwder_style::icon32 ok PROGMEM;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
enum class special_key {
|
||||||
|
RETURN, TAB
|
||||||
|
};
|
||||||
|
|
||||||
|
void usb_keyboard_init();
|
||||||
|
void usb_send_key(special_key k);
|
||||||
|
void usb_send_key(char c);
|
||||||
|
|
||||||
|
void ble_keyboard_init();
|
||||||
|
bool ble_keyboard_ready();
|
||||||
|
void ble_keyboard_update_battery_level();
|
||||||
|
void ble_send_key(special_key k);
|
||||||
|
void ble_send_key(char c);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
void check_keyboard_events();
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
void retry_connection(bool reconnect = true);
|
||||||
|
// void read_response(WiFiClient* client, String& lines);
|
||||||
|
// void net_password_import();
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// pwrotocol - PWDer protocol for KeePass communication
|
||||||
|
// version 0.5
|
||||||
|
// let's see
|
||||||
|
|
||||||
|
#define PWROTOCOL_TIMEOUT_MS 10000
|
||||||
|
#define PWROTOCOL_IMPORT_MAGIC "PWIMPORT"
|
||||||
|
|
||||||
|
#define PWROTOCOL_PING 0x01
|
||||||
|
#define PWROTOCOL_GET_SALT 0x02
|
||||||
|
#define PWROTOCOL_GET_ITERATIONS 0x03
|
||||||
|
#define PWROTOCOL_FILE_IMPORT 0x04
|
||||||
|
#define PWROTOCOL_REQUEST_CONF 0x05
|
||||||
|
|
||||||
|
#define PWROTOCOL_CHUNK_SIZE 64 // bytes
|
||||||
|
|
||||||
|
void pwrotocol_listen_and_respond();
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <M5Cardputer.h>
|
||||||
|
#include <pvault.h>
|
||||||
|
|
||||||
|
namespace sfx {
|
||||||
|
void beep();
|
||||||
|
void melody_c_major_scale();
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
unsigned long to_unix_utc_timestamp(int year, int month, int day, int hour, int minute, int second);
|
||||||
|
struct tm to_regular_utc_timestamp(unsigned long epoch);
|
||||||
|
void generate_totp(String secret);
|
||||||
|
bool start_rtc();
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#define PWDER_VERSION "v0.6 \"Repainted\""
|
||||||
|
#define CREDITS_PWDER "PWDer 0.6 \"Repainted\" >"
|
||||||
|
|
||||||
|
// English
|
||||||
|
#ifdef lang_en
|
||||||
|
#define PASSWORD_MANAGER_TITLEBAR "Passwords"
|
||||||
|
#define LOGIN_TITLEBAR "Enter password"
|
||||||
|
#define HANDBOOK_TITLEBAR "Help"
|
||||||
|
#define OPTIONS_TITLEBAR "Options"
|
||||||
|
#define SYNC_TITLEBAR "Synchronization"
|
||||||
|
#define CREDITS_TITLEBAR "About"
|
||||||
|
#define PWIMPORT_TITLEBAR "New passwords found"
|
||||||
|
#define SEARCH_TITLEBAR "Search"
|
||||||
|
|
||||||
|
const String handbook[22] PROGMEM = {
|
||||||
|
"ESC: Exit", "< Navigate >",
|
||||||
|
"Sometimes you need to use", "FN with ESC and arrow keys",
|
||||||
|
"H: Open this handbook", "OK: Input with default mode",
|
||||||
|
"1: Enter login", "2: Enter password",
|
||||||
|
"3: Login-TAB-Password-Enter", "4: Enter OTP if available",
|
||||||
|
"T: Press TAB on computer", "R: Press ENTER on computer",
|
||||||
|
"V: Preview credential", "Q: Search",
|
||||||
|
"B: Connect Bluetooth", "N: Connect Wi-Fi",
|
||||||
|
"L: Lock device", "M: Mute speaker",
|
||||||
|
"O: Open options", "C: Open About page",
|
||||||
|
"FN + Top button:", "Take a screenshot"
|
||||||
|
};
|
||||||
|
|
||||||
|
#define OPTIONS_BRIGHTNESS "Brightness:"
|
||||||
|
#define OPTIONS_BRIGHTNESS_PERCENTAGE "%"
|
||||||
|
#define OPTIONS_DEFAULT_USERNAME "Default input mode: username"
|
||||||
|
#define OPTIONS_DEFAULT_PASSWORD "Default input mode: password"
|
||||||
|
#define OPTIONS_DEFAULT_FULL "Default input mode: full"
|
||||||
|
#define OPTIONS_AUTOLOCK_ON "Inactivity lock: On"
|
||||||
|
#define OPTIONS_AUTOLOCK_OFF "Inactivity lock: Off"
|
||||||
|
#define OPTIONS_COLOR_SCHEME "Theme:"
|
||||||
|
#define OPTIONS_DEVICE_PASSWORD "Password:"
|
||||||
|
#define OPTIONS_RTC_TIME "RTC Time: "
|
||||||
|
#define OPTIONS_RTC_NTPSYNC "Press OK to sync with NTP"
|
||||||
|
#define OPTIONS_RTC_NTPSYNC_RTC_UNAVAILABLE "Sync unavailable: No RTC"
|
||||||
|
#define OPTIONS_SSID "Wi-Fi SSID:"
|
||||||
|
#define OPTIONS_WIFI_PASSWORD "Wi-Fi Key:"
|
||||||
|
|
||||||
|
#define SYNC_WIFI_CONNECTING_PHASE "Connecting to Wi-Fi"
|
||||||
|
#define SYNC_SERVER_CONNECTING_PHASE "Connecting to server"
|
||||||
|
#define SYNC_DOWNLOAD_PHASE "Downloading data"
|
||||||
|
#define SYNC_OK "Sync OK, disconnecting"
|
||||||
|
#define SYNC_RETURN "Returning in 3 seconds"
|
||||||
|
#define SYNC_ERR "Sync error:"
|
||||||
|
#define SYNC_ERR_DESCRIPTION_WIFI "Wi-Fi error"
|
||||||
|
#define SYNC_ERR_DESCRIPTION_SERVER "Server error"
|
||||||
|
#define SYNC_ERR_DESCRIPTION_FILE "File error"
|
||||||
|
|
||||||
|
#define CREDITS_WEBSITE "< Visit my website!"
|
||||||
|
#define CREDITS_PWDER_GITHUB "adamecki/PWDer on GitHub"
|
||||||
|
#define CREDITS_WEBSITE_LINK "floriano.uk"
|
||||||
|
|
||||||
|
#define PWIMPORT_REPLACE_PASSWORDS "Replace current passwords?"
|
||||||
|
|
||||||
|
#define SEARCH_NO_RESULTS "No results"
|
||||||
|
|
||||||
|
#define SDCARD_NOT_FOUND_TITLE "Error"
|
||||||
|
#define SDCARD_NOT_FOUND_DESCRIPTION "SD card not found!"
|
||||||
|
#define CONNECTION_LIMIT_TITLE "Can't establish this connection"
|
||||||
|
#define CONNECTION_LIMIT_DESCRIPTION "Restart PWDer to activate"
|
||||||
|
|
||||||
|
#define SAMPLE_ENTRY "Sample Entry"
|
||||||
|
#define SAMPLE_USERNAME "sample_user"
|
||||||
|
#define SAMPLE_PASSWORD "sample_password"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Polish
|
||||||
|
#ifdef lang_pl
|
||||||
|
#define PASSWORD_MANAGER_TITLEBAR "Moje hasla"
|
||||||
|
#define LOGIN_TITLEBAR "Podaj haslo"
|
||||||
|
#define HANDBOOK_TITLEBAR "Pomoc"
|
||||||
|
#define OPTIONS_TITLEBAR "Opcje"
|
||||||
|
#define SYNC_TITLEBAR "Synchronizacja"
|
||||||
|
#define CREDITS_TITLEBAR "O programie"
|
||||||
|
#define PWIMPORT_TITLEBAR "Znaleziono nowe hasla"
|
||||||
|
#define SEARCH_TITLEBAR "Wyszukaj"
|
||||||
|
|
||||||
|
const String handbook[22] PROGMEM = {
|
||||||
|
"ESC: Wyjdz", "< Nawiguj >",
|
||||||
|
"Czasami z przyciskami strzalek", "oraz ESC nalezy uzyc FN",
|
||||||
|
"H: Otworz tego pomocnika", "OK: Wprowadz zgodnie z ust. dom.",
|
||||||
|
"1: Wprowadz login", "2: Wprowadz haslo",
|
||||||
|
"3: Login-TAB-Haslo-Enter", "4: Wprowadz OTP jesli dostepne",
|
||||||
|
"T: Wcisnij TAB na komputerze", "R: Wcisnij ENTER na komputerze",
|
||||||
|
"V: Podglad wpisu", "Q: Wyszukaj",
|
||||||
|
"B: Polacz Bluetooth", "N: Polacz Wi-Fi",
|
||||||
|
"L: Zablokuj urzadzenie", "M: Wycisz dzwiek",
|
||||||
|
"O: Otworz ustawienia", "C: Otworz \"O programie\"",
|
||||||
|
"FN + Gorny przycisk:", "Zrzut ekranu"
|
||||||
|
};
|
||||||
|
|
||||||
|
#define OPTIONS_BRIGHTNESS "Jasnosc:"
|
||||||
|
#define OPTIONS_BRIGHTNESS_PERCENTAGE "%"
|
||||||
|
#define OPTIONS_DEFAULT_USERNAME "Domyslnie wprowadz: nazwe uzytkownika"
|
||||||
|
#define OPTIONS_DEFAULT_PASSWORD "Domyslnie wprowadz: haslo"
|
||||||
|
#define OPTIONS_DEFAULT_FULL "Domyslnie wprowadz: wszystko"
|
||||||
|
#define OPTIONS_AUTOLOCK_ON "Blokuj gdy nieaktywny: Tak"
|
||||||
|
#define OPTIONS_AUTOLOCK_OFF "Blokuj gdy nieaktywny: Nie"
|
||||||
|
#define OPTIONS_COLOR_SCHEME "Motyw:"
|
||||||
|
#define OPTIONS_DEVICE_PASSWORD "Haslo glowne:"
|
||||||
|
#define OPTIONS_RTC_TIME "Czas RTC: "
|
||||||
|
#define OPTIONS_RTC_NTPSYNC "OK - synchronizacja z NTP"
|
||||||
|
#define OPTIONS_RTC_NTPSYNC_RTC_UNAVAILABLE "Sync. niedostepna: Brak RTC"
|
||||||
|
#define OPTIONS_SSID "SSID Wi-Fi:"
|
||||||
|
#define OPTIONS_WIFI_PASSWORD "Haslo Wi-Fi:"
|
||||||
|
|
||||||
|
#define SYNC_WIFI_CONNECTING_PHASE "Laczenie z Wi-Fi"
|
||||||
|
#define SYNC_SERVER_CONNECTING_PHASE "Laczenie z serwerem"
|
||||||
|
#define SYNC_DOWNLOAD_PHASE "Pobieranie danych"
|
||||||
|
#define SYNC_OK "Synchr. OK, rozlaczanie"
|
||||||
|
#define SYNC_RETURN "Powrot za 3 sekundy"
|
||||||
|
#define SYNC_ERR "Blad synchronizacji:"
|
||||||
|
#define SYNC_ERR_DESCRIPTION_WIFI "Problem z Wi-Fi"
|
||||||
|
#define SYNC_ERR_DESCRIPTION_SERVER "Problem z serwerem"
|
||||||
|
#define SYNC_ERR_DESCRIPTION_FILE "Problem z plikiem"
|
||||||
|
|
||||||
|
#define CREDITS_WEBSITE "< Zobacz moja strone! >"
|
||||||
|
#define CREDITS_PWDER_GITHUB "adamecki/PWDer na GitHub"
|
||||||
|
#define CREDITS_WEBSITE_LINK "floriano.uk"
|
||||||
|
|
||||||
|
#define PWIMPORT_REPLACE_PASSWORDS "Zastapic obecne hasla?"
|
||||||
|
|
||||||
|
#define SEARCH_NO_RESULTS "Brak wynikow"
|
||||||
|
|
||||||
|
#define SDCARD_NOT_FOUND_TITLE "Blad"
|
||||||
|
#define SDCARD_NOT_FOUND_DESCRIPTION "Nie znaleziono karty SD!"
|
||||||
|
#define CONNECTION_LIMIT_TITLE "Nie mozna nawiazac polaczenia"
|
||||||
|
#define CONNECTION_LIMIT_DESCRIPTION "Zrestartuj PWDer, by aktywowac"
|
||||||
|
|
||||||
|
#define SAMPLE_ENTRY "Przykladowy wpis"
|
||||||
|
#define SAMPLE_USERNAME "przykladowa_nazwa_uzytkownika"
|
||||||
|
#define SAMPLE_PASSWORD "przykladowe_haslo"
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
#include "cryptographic_operations.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <mbedtls/md.h>
|
||||||
|
#include <mbedtls/gcm.h>
|
||||||
|
#include <mbedtls/pkcs5.h>
|
||||||
|
#include <esp_system.h>
|
||||||
|
|
||||||
|
namespace pvault_cryptography {
|
||||||
|
bool generate_salt(uint8_t* salt) {
|
||||||
|
if(salt == nullptr) { return false; }
|
||||||
|
esp_fill_random(salt, pvault::salt_size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool generate_nonce(uint8_t* nonce) {
|
||||||
|
if(nonce == nullptr) { return false; }
|
||||||
|
esp_fill_random(nonce, pvault::nonce_size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool derive_key(const String& password, const uint8_t* salt, uint8_t* key) {
|
||||||
|
// argument validation
|
||||||
|
if(salt == nullptr || key == nullptr) { return false; }
|
||||||
|
|
||||||
|
// init ESP32 cryptography
|
||||||
|
mbedtls_md_context_t md_ctx;
|
||||||
|
mbedtls_md_init(&md_ctx);
|
||||||
|
|
||||||
|
// SHA256 algorithm descriptions
|
||||||
|
const mbedtls_md_info_t* md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||||
|
if(md_info == nullptr) { return false; }
|
||||||
|
|
||||||
|
// create HMAC context
|
||||||
|
int result = mbedtls_md_setup(&md_ctx, md_info, 1);
|
||||||
|
if(result != 0) {
|
||||||
|
mbedtls_md_free(&md_ctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate AES128 key from provided password
|
||||||
|
result = mbedtls_pkcs5_pbkdf2_hmac(
|
||||||
|
&md_ctx,
|
||||||
|
reinterpret_cast<const unsigned char*>(password.c_str()),
|
||||||
|
password.length(),
|
||||||
|
salt,
|
||||||
|
pvault::salt_size,
|
||||||
|
pvault::iterations,
|
||||||
|
pvault::key_size,
|
||||||
|
key
|
||||||
|
);
|
||||||
|
|
||||||
|
// free HMAC context
|
||||||
|
mbedtls_md_free(&md_ctx);
|
||||||
|
|
||||||
|
if(result != 0) {
|
||||||
|
secure_zero(key, pvault::key_size);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool decrypt_verify(
|
||||||
|
const uint8_t* key,
|
||||||
|
const uint8_t* nonce,
|
||||||
|
File& file,
|
||||||
|
const uint32_t ciphertext_length,
|
||||||
|
const uint8_t* tag,
|
||||||
|
const uint32_t header_size
|
||||||
|
) {
|
||||||
|
// argument validation
|
||||||
|
if(!key || !nonce || !file || !tag) { return false; }
|
||||||
|
|
||||||
|
// GCM init
|
||||||
|
mbedtls_gcm_context gcm;
|
||||||
|
mbedtls_gcm_init(&gcm);
|
||||||
|
|
||||||
|
int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 128);
|
||||||
|
if(ret != 0) {
|
||||||
|
mbedtls_gcm_free(&gcm);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.seek(header_size);
|
||||||
|
|
||||||
|
// buffers
|
||||||
|
uint8_t chunk_bytes[CHUNK_SIZE];
|
||||||
|
uint32_t bytes_remaining = ciphertext_length;
|
||||||
|
|
||||||
|
mbedtls_gcm_starts(&gcm, MBEDTLS_GCM_DECRYPT, nonce, pvault::nonce_size, NULL, 0);
|
||||||
|
|
||||||
|
uint32_t read_size;
|
||||||
|
|
||||||
|
while(bytes_remaining > 0) {
|
||||||
|
if(bytes_remaining > CHUNK_SIZE) {
|
||||||
|
read_size = CHUNK_SIZE;
|
||||||
|
} else {
|
||||||
|
read_size = bytes_remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.read(chunk_bytes, read_size);
|
||||||
|
|
||||||
|
mbedtls_gcm_update(&gcm, read_size, chunk_bytes, chunk_bytes);
|
||||||
|
|
||||||
|
bytes_remaining -= read_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t computed_tag[pvault::tag_size];
|
||||||
|
mbedtls_gcm_finish(&gcm, computed_tag, pvault::tag_size);
|
||||||
|
mbedtls_gcm_free(&gcm);
|
||||||
|
|
||||||
|
return memcmp(tag, computed_tag, pvault::tag_size) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool encrypt_chunked(
|
||||||
|
const uint8_t* key,
|
||||||
|
const uint8_t* nonce,
|
||||||
|
const uint8_t* plaintext,
|
||||||
|
uint32_t plaintext_length,
|
||||||
|
File& file
|
||||||
|
) {
|
||||||
|
// argument validation
|
||||||
|
if(!key || !nonce || !plaintext || !file) { return false; }
|
||||||
|
|
||||||
|
// buffers
|
||||||
|
uint8_t tag[pvault::tag_size];
|
||||||
|
uint8_t chunk_bytes[CHUNK_SIZE];
|
||||||
|
|
||||||
|
// GCM init
|
||||||
|
mbedtls_gcm_context gcm;
|
||||||
|
mbedtls_gcm_init(&gcm);
|
||||||
|
|
||||||
|
int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 128);
|
||||||
|
if(ret != 0) {
|
||||||
|
mbedtls_gcm_free(&gcm);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// encryption
|
||||||
|
mbedtls_gcm_starts(&gcm, MBEDTLS_GCM_ENCRYPT, nonce, pvault::nonce_size, NULL, 0);
|
||||||
|
|
||||||
|
uint32_t remaining = plaintext_length;
|
||||||
|
uint32_t read_offset = 0;
|
||||||
|
|
||||||
|
while(remaining > 0) {
|
||||||
|
uint32_t n;
|
||||||
|
|
||||||
|
if(remaining > CHUNK_SIZE) {
|
||||||
|
n = CHUNK_SIZE;
|
||||||
|
} else {
|
||||||
|
n = remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
mbedtls_gcm_update(&gcm, n, plaintext + read_offset, chunk_bytes);
|
||||||
|
file.write(chunk_bytes, n);
|
||||||
|
|
||||||
|
read_offset += n;
|
||||||
|
remaining -= n;
|
||||||
|
}
|
||||||
|
|
||||||
|
mbedtls_gcm_finish(&gcm, tag, pvault::tag_size);
|
||||||
|
mbedtls_gcm_free(&gcm);
|
||||||
|
file.write(tag, pvault::tag_size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool decrypt_chunked( // will return junk if get_key earlier returned false but program still decided to decrypt
|
||||||
|
const uint8_t* key,
|
||||||
|
const uint8_t* nonce,
|
||||||
|
File& file,
|
||||||
|
uint32_t ciphertext_length,
|
||||||
|
uint8_t* plaintext
|
||||||
|
) {
|
||||||
|
// argument validation
|
||||||
|
if(!key || !nonce || !file || !plaintext) { return false; }
|
||||||
|
|
||||||
|
// buffers
|
||||||
|
uint32_t file_size = file.size();
|
||||||
|
uint8_t tag[pvault::tag_size];
|
||||||
|
uint8_t chunk_bytes[CHUNK_SIZE];
|
||||||
|
|
||||||
|
// read tag
|
||||||
|
file.seek(file_size - pvault::tag_size);
|
||||||
|
file.read(tag, pvault::tag_size);
|
||||||
|
|
||||||
|
// GCM init
|
||||||
|
mbedtls_gcm_context gcm;
|
||||||
|
mbedtls_gcm_init(&gcm);
|
||||||
|
|
||||||
|
int ret = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, 128);
|
||||||
|
if(ret != 0) {
|
||||||
|
mbedtls_gcm_free(&gcm);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// decryption
|
||||||
|
mbedtls_gcm_starts(&gcm, MBEDTLS_GCM_DECRYPT, nonce, pvault::nonce_size, NULL, 0);
|
||||||
|
|
||||||
|
// seek back to ciphertext start
|
||||||
|
file.seek(sizeof(pvault::header));
|
||||||
|
|
||||||
|
uint32_t bytes_remaining = ciphertext_length;
|
||||||
|
uint32_t write_offset = 0;
|
||||||
|
uint32_t read_size;
|
||||||
|
|
||||||
|
while(bytes_remaining > 0) {
|
||||||
|
if(bytes_remaining > CHUNK_SIZE) {
|
||||||
|
read_size = CHUNK_SIZE;
|
||||||
|
} else {
|
||||||
|
read_size = bytes_remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.read(chunk_bytes, read_size);
|
||||||
|
|
||||||
|
mbedtls_gcm_update(&gcm, read_size, chunk_bytes, plaintext + write_offset);
|
||||||
|
|
||||||
|
write_offset += read_size;
|
||||||
|
bytes_remaining -= read_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
mbedtls_gcm_finish(&gcm, tag, pvault::tag_size);
|
||||||
|
mbedtls_gcm_free(&gcm);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool write_header(File& file, const pvault::header& hdr) {
|
||||||
|
// file validation
|
||||||
|
if(!file) { return false; }
|
||||||
|
|
||||||
|
// write header
|
||||||
|
uint32_t written = file.write(reinterpret_cast<const uint8_t*>(&hdr), sizeof(pvault::header));
|
||||||
|
|
||||||
|
// validate written data
|
||||||
|
if(written != sizeof(pvault::header)) { return false; }
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_header(File& file, pvault::header& hdr) {
|
||||||
|
// file validation
|
||||||
|
if(!file) { return false; }
|
||||||
|
|
||||||
|
// read binary header
|
||||||
|
uint32_t read_bytes = file.read(reinterpret_cast<uint8_t*>(&hdr), sizeof(pvault::header));
|
||||||
|
|
||||||
|
// validate read data
|
||||||
|
if(read_bytes != sizeof(pvault::header)) { return false; }
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool write_tag(File& file, const uint8_t* tag) {
|
||||||
|
// file and tag validation
|
||||||
|
if(!file || !tag) { return false; }
|
||||||
|
|
||||||
|
// write binary tag
|
||||||
|
uint32_t written = file.write(tag, pvault::tag_size);
|
||||||
|
|
||||||
|
// validate written data
|
||||||
|
if(written != pvault::tag_size) { return false; }
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_tag(File& file, uint8_t* tag) {
|
||||||
|
// file and tag validation
|
||||||
|
if(!file || !tag) { return false; }
|
||||||
|
|
||||||
|
// read binary tag
|
||||||
|
uint32_t read_bytes = file.read(tag, pvault::tag_size);
|
||||||
|
|
||||||
|
// validate read data
|
||||||
|
if(read_bytes != pvault::tag_size) { return false; }
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void secure_zero(void* ptr, uint32_t size) {
|
||||||
|
volatile uint8_t* p = reinterpret_cast<volatile uint8_t*>(ptr);
|
||||||
|
|
||||||
|
while(size--) {
|
||||||
|
*p++ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
#include "pvault.h"
|
||||||
|
|
||||||
|
#define CHUNK_SIZE 1024 // bytes
|
||||||
|
|
||||||
|
namespace pvault_cryptography {
|
||||||
|
// generate values
|
||||||
|
bool generate_salt(uint8_t* salt);
|
||||||
|
bool generate_nonce(uint8_t* nonce);
|
||||||
|
|
||||||
|
// cryptography
|
||||||
|
bool derive_key(const String& password, const uint8_t* salt, uint8_t* key); // Derive key from password
|
||||||
|
|
||||||
|
bool decrypt_verify( // lightweight function to check if used key works with the ciphertext
|
||||||
|
const uint8_t* key,
|
||||||
|
const uint8_t* nonce,
|
||||||
|
File& file,
|
||||||
|
const uint32_t ciphertext_length,
|
||||||
|
const uint8_t* tag,
|
||||||
|
uint32_t header_size
|
||||||
|
);
|
||||||
|
|
||||||
|
bool encrypt_chunked( // writes ciphertext + tag to provided file
|
||||||
|
const uint8_t* key,
|
||||||
|
const uint8_t* nonce,
|
||||||
|
const uint8_t* plaintext,
|
||||||
|
uint32_t plaintext_length,
|
||||||
|
File& file
|
||||||
|
);
|
||||||
|
|
||||||
|
bool decrypt_chunked( // reads tag itself + plaintext to provided pointer
|
||||||
|
const uint8_t* key,
|
||||||
|
const uint8_t* nonce,
|
||||||
|
File& file,
|
||||||
|
uint32_t ciphertext_length,
|
||||||
|
uint8_t* plaintext
|
||||||
|
);
|
||||||
|
|
||||||
|
// vault file structure
|
||||||
|
bool write_header(File& file, const pvault::header& hdr);
|
||||||
|
bool read_header(File& file, pvault::header& hdr); // assumes that file pointer is set at the beginning of the file
|
||||||
|
|
||||||
|
bool write_tag(File& file, const uint8_t* tag);
|
||||||
|
bool read_tag(File& file, uint8_t* tag); // assumes that file pointer is set on tag's location
|
||||||
|
|
||||||
|
// memory security
|
||||||
|
void secure_zero(void* ptr, uint32_t size);
|
||||||
|
}
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
#include "pvault.h"
|
||||||
|
#include "cryptographic_operations.h"
|
||||||
|
|
||||||
|
namespace pvault {
|
||||||
|
bool get_key(
|
||||||
|
const char* path,
|
||||||
|
const String& password,
|
||||||
|
uint8_t* key
|
||||||
|
) {
|
||||||
|
File file = SD.open(path, FILE_READ);
|
||||||
|
if(!file) { return false; }
|
||||||
|
|
||||||
|
// get master salt and nonce
|
||||||
|
header hdr{};
|
||||||
|
pvault_cryptography::read_header(file, hdr);
|
||||||
|
|
||||||
|
// obtain key candidate
|
||||||
|
pvault_cryptography::derive_key(password, hdr.master_salt, key);
|
||||||
|
|
||||||
|
uint32_t len = hdr.ciphertext_length;
|
||||||
|
|
||||||
|
// get tag
|
||||||
|
uint8_t tag[pvault::tag_size];
|
||||||
|
uint32_t file_size = file.size();
|
||||||
|
file.seek(file_size - pvault::tag_size);
|
||||||
|
file.read(tag, pvault::tag_size);
|
||||||
|
|
||||||
|
bool decryption = pvault_cryptography::decrypt_verify(key, hdr.nonce, file, len, tag, sizeof(pvault::header));
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
return decryption;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool init_vault(
|
||||||
|
const char* path,
|
||||||
|
const String& password,
|
||||||
|
const device_settings& settings,
|
||||||
|
const vault& data
|
||||||
|
) {
|
||||||
|
// input validation
|
||||||
|
if(!path || password.length() == 0) { return false; }
|
||||||
|
|
||||||
|
// open file
|
||||||
|
if(SD.exists(path)) { SD.remove(path); }
|
||||||
|
File file = SD.open(path, FILE_WRITE);
|
||||||
|
if(!file) { return false; }
|
||||||
|
|
||||||
|
// buffers
|
||||||
|
header hdr{};
|
||||||
|
uint8_t salt[salt_size];
|
||||||
|
uint8_t nonce[nonce_size];
|
||||||
|
uint8_t key[key_size];
|
||||||
|
uint32_t len = sizeof(data);
|
||||||
|
|
||||||
|
// generate parameters
|
||||||
|
pvault_cryptography::generate_salt(salt);
|
||||||
|
pvault_cryptography::generate_nonce(nonce);
|
||||||
|
pvault_cryptography::derive_key(password, salt, key);
|
||||||
|
|
||||||
|
// build header
|
||||||
|
hdr.magic[0] = 'C';
|
||||||
|
hdr.magic[1] = 'R';
|
||||||
|
hdr.magic[2] = 'Y';
|
||||||
|
hdr.magic[3] = 'P';
|
||||||
|
|
||||||
|
hdr.version = file_version;
|
||||||
|
hdr.iterations = iterations;
|
||||||
|
|
||||||
|
memcpy(hdr.master_salt, salt, salt_size);
|
||||||
|
memcpy(hdr.nonce, nonce, nonce_size);
|
||||||
|
|
||||||
|
hdr.settings = settings;
|
||||||
|
|
||||||
|
hdr.ciphertext_length = len;
|
||||||
|
|
||||||
|
pvault_cryptography::write_header(file, hdr);
|
||||||
|
|
||||||
|
// encrypt
|
||||||
|
bool encryption = pvault_cryptography::encrypt_chunked(
|
||||||
|
key,
|
||||||
|
nonce,
|
||||||
|
reinterpret_cast<const uint8_t*>(&data),
|
||||||
|
len,
|
||||||
|
file
|
||||||
|
);
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
pvault_cryptography::secure_zero(salt, salt_size);
|
||||||
|
pvault_cryptography::secure_zero(nonce, nonce_size);
|
||||||
|
pvault_cryptography::secure_zero(key, key_size);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
return encryption;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool update_vault(
|
||||||
|
const char* path,
|
||||||
|
const uint8_t* key,
|
||||||
|
const device_settings& settings,
|
||||||
|
const vault& data
|
||||||
|
) {
|
||||||
|
// input validation
|
||||||
|
if(!path || !key) { return false; }
|
||||||
|
|
||||||
|
// open file
|
||||||
|
File read = SD.open(path, FILE_READ);
|
||||||
|
if(!read) { return false; }
|
||||||
|
|
||||||
|
// read header
|
||||||
|
header hdr{};
|
||||||
|
if(!pvault_cryptography::read_header(read, hdr)) {
|
||||||
|
read.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// close file (we have everything needed)
|
||||||
|
read.close();
|
||||||
|
|
||||||
|
// write header and encrypted data
|
||||||
|
SD.remove(path);
|
||||||
|
File write = SD.open(path, FILE_WRITE);
|
||||||
|
if(!write) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate new nonce
|
||||||
|
pvault_cryptography::generate_nonce(hdr.nonce);
|
||||||
|
|
||||||
|
// modify header
|
||||||
|
hdr.settings = settings;
|
||||||
|
|
||||||
|
pvault_cryptography::write_header(write, hdr);
|
||||||
|
|
||||||
|
// encrypt
|
||||||
|
uint32_t len = sizeof(data);
|
||||||
|
|
||||||
|
bool encryption = pvault_cryptography::encrypt_chunked(
|
||||||
|
key,
|
||||||
|
hdr.nonce,
|
||||||
|
reinterpret_cast<const uint8_t*>(&data),
|
||||||
|
len,
|
||||||
|
write
|
||||||
|
);
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
write.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool load_vault(
|
||||||
|
const char* path,
|
||||||
|
const uint8_t* key,
|
||||||
|
device_settings& settings,
|
||||||
|
vault& data
|
||||||
|
) {
|
||||||
|
// input validation
|
||||||
|
if(!path || !key) { return false; }
|
||||||
|
|
||||||
|
// open file
|
||||||
|
File file = SD.open(path, FILE_READ);
|
||||||
|
if(!file) { return false; }
|
||||||
|
|
||||||
|
// read header
|
||||||
|
header hdr{};
|
||||||
|
if(!pvault_cryptography::read_header(file, hdr)) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// header validation
|
||||||
|
if(hdr.magic[0] != 'C' || hdr.magic[1] != 'R' || hdr.magic[2] != 'Y' || hdr.magic[3] != 'P') {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hdr.version != file_version) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hdr.ciphertext_length == 0) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings = hdr.settings;
|
||||||
|
|
||||||
|
// buffers
|
||||||
|
uint32_t len = hdr.ciphertext_length;
|
||||||
|
uint8_t nonce[nonce_size];
|
||||||
|
|
||||||
|
// parameters
|
||||||
|
memcpy(nonce, hdr.nonce, nonce_size);
|
||||||
|
|
||||||
|
bool decryption = pvault_cryptography::decrypt_chunked(
|
||||||
|
key,
|
||||||
|
nonce,
|
||||||
|
file,
|
||||||
|
len,
|
||||||
|
reinterpret_cast<uint8_t*>(&data)
|
||||||
|
);
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
file.close();
|
||||||
|
return decryption;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool update_config(
|
||||||
|
const char* path,
|
||||||
|
const device_settings& settings
|
||||||
|
) {
|
||||||
|
// open the old file
|
||||||
|
File old_file = SD.open(path, FILE_READ);
|
||||||
|
if(!old_file) { return false; }
|
||||||
|
|
||||||
|
// read its header
|
||||||
|
header hdr{};
|
||||||
|
if(!pvault_cryptography::read_header(old_file, hdr)) {
|
||||||
|
old_file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update settings in header
|
||||||
|
hdr.settings = settings;
|
||||||
|
|
||||||
|
// obtain length and add tag size to it
|
||||||
|
uint32_t len = hdr.ciphertext_length;
|
||||||
|
len += pvault::tag_size;
|
||||||
|
|
||||||
|
// open temp file
|
||||||
|
String temp_path = String(path);
|
||||||
|
temp_path += ".temp";
|
||||||
|
|
||||||
|
if(SD.exists(temp_path)) { SD.remove(temp_path); }
|
||||||
|
File new_file = SD.open(temp_path, FILE_WRITE);
|
||||||
|
if(!new_file) {
|
||||||
|
old_file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewrite header and remaining bytes
|
||||||
|
if(!pvault_cryptography::write_header(new_file, hdr)) {
|
||||||
|
new_file.close();
|
||||||
|
old_file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t to_write = len;
|
||||||
|
uint32_t write_size;
|
||||||
|
|
||||||
|
uint8_t buffer[CHUNK_SIZE];
|
||||||
|
|
||||||
|
while(to_write > 0) {
|
||||||
|
if(to_write > CHUNK_SIZE) {
|
||||||
|
write_size = CHUNK_SIZE;
|
||||||
|
} else {
|
||||||
|
write_size = to_write;
|
||||||
|
}
|
||||||
|
|
||||||
|
old_file.read(buffer, write_size);
|
||||||
|
new_file.write(buffer, write_size);
|
||||||
|
|
||||||
|
to_write -= write_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
old_file.close();
|
||||||
|
new_file.close();
|
||||||
|
|
||||||
|
// delete old file and replace with new
|
||||||
|
SD.remove(path);
|
||||||
|
if(SD.exists(temp_path)) { SD.rename(temp_path, path); }
|
||||||
|
|
||||||
|
// exit
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_config(
|
||||||
|
const char* path,
|
||||||
|
device_settings& settings
|
||||||
|
) {
|
||||||
|
File file = SD.open(path, FILE_READ);
|
||||||
|
if(!file) { return false; }
|
||||||
|
|
||||||
|
header hdr{};
|
||||||
|
pvault_cryptography::read_header(file, hdr);
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
settings = hdr.settings;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_salt(
|
||||||
|
const char* path,
|
||||||
|
uint8_t* salt
|
||||||
|
) {
|
||||||
|
if (!path) { return false; }
|
||||||
|
|
||||||
|
File file = SD.open(path, FILE_READ);
|
||||||
|
if(!file) { return false; }
|
||||||
|
header hdr{};
|
||||||
|
if(!pvault_cryptography::read_header(file, hdr)) {
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(salt, hdr.master_salt, pvault::salt_size);
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool replace_vault(
|
||||||
|
const char* path,
|
||||||
|
const device_settings& settings,
|
||||||
|
|
||||||
|
const uint8_t* key,
|
||||||
|
const uint8_t* salt,
|
||||||
|
|
||||||
|
const char* import_path
|
||||||
|
) {
|
||||||
|
File import_file = SD.open(import_path, FILE_READ);
|
||||||
|
// read file header
|
||||||
|
// magic 8 bytes
|
||||||
|
uint8_t magic[8];
|
||||||
|
import_file.read(magic, 8);
|
||||||
|
|
||||||
|
if(memcmp(magic, "PWIMPORT", 8) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nonce 12 bytes
|
||||||
|
uint8_t nonce[pvault::nonce_size];
|
||||||
|
import_file.read(nonce, pvault::nonce_size);
|
||||||
|
|
||||||
|
// length 4 bytes
|
||||||
|
uint32_t len;
|
||||||
|
import_file.read(reinterpret_cast<uint8_t*>(&len), sizeof(uint32_t));
|
||||||
|
|
||||||
|
// read tag
|
||||||
|
uint32_t file_size = import_file.size();
|
||||||
|
uint8_t tag[pvault::tag_size];
|
||||||
|
import_file.seek(file_size - pvault::tag_size);
|
||||||
|
import_file.read(tag, pvault::tag_size);
|
||||||
|
|
||||||
|
// check if key that we have can decrypt the imported vault. Otherwise there's no point doing it.
|
||||||
|
bool decryption = pvault_cryptography::decrypt_verify(key, nonce, import_file, len, tag, 24);
|
||||||
|
if(!decryption) { return false; }
|
||||||
|
|
||||||
|
// overwrite vault with provided settings
|
||||||
|
header hdr{};
|
||||||
|
|
||||||
|
hdr.magic[0] = 'C';
|
||||||
|
hdr.magic[1] = 'R';
|
||||||
|
hdr.magic[2] = 'Y';
|
||||||
|
hdr.magic[3] = 'P';
|
||||||
|
|
||||||
|
hdr.iterations = pvault::iterations;
|
||||||
|
hdr.settings = settings;
|
||||||
|
|
||||||
|
hdr.ciphertext_length = len;
|
||||||
|
hdr.version = pvault::file_version;
|
||||||
|
memcpy(hdr.master_salt, salt, pvault::salt_size);
|
||||||
|
memcpy(hdr.nonce, nonce, pvault::nonce_size);
|
||||||
|
|
||||||
|
if(SD.exists(path)) { SD.remove(path); }
|
||||||
|
File new_file = SD.open(path, FILE_WRITE);
|
||||||
|
|
||||||
|
pvault_cryptography::write_header(new_file, hdr);
|
||||||
|
|
||||||
|
// copy ciphertext from old file to new
|
||||||
|
uint32_t to_copy = len;
|
||||||
|
uint32_t copy_size;
|
||||||
|
uint8_t copy_buffer[CHUNK_SIZE];
|
||||||
|
|
||||||
|
import_file.seek(24);
|
||||||
|
|
||||||
|
while(to_copy > 0) {
|
||||||
|
if(to_copy > CHUNK_SIZE) {
|
||||||
|
copy_size = CHUNK_SIZE;
|
||||||
|
} else {
|
||||||
|
copy_size = to_copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
import_file.read(copy_buffer, copy_size);
|
||||||
|
new_file.write(copy_buffer, copy_size);
|
||||||
|
|
||||||
|
to_copy -= copy_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
pvault_cryptography::write_tag(new_file, tag);
|
||||||
|
|
||||||
|
import_file.close();
|
||||||
|
new_file.close();
|
||||||
|
|
||||||
|
// remove import file
|
||||||
|
SD.remove(import_path);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
// PWDer vault file format 1.0
|
||||||
|
// and functions for its operation
|
||||||
|
// let's see
|
||||||
|
|
||||||
|
namespace pvault {
|
||||||
|
constexpr uint8_t magic_size = 4;
|
||||||
|
constexpr uint8_t file_version = 1;
|
||||||
|
|
||||||
|
constexpr uint32_t iterations = 10000;
|
||||||
|
constexpr uint8_t salt_size = 16;
|
||||||
|
constexpr uint8_t nonce_size = 12;
|
||||||
|
|
||||||
|
constexpr uint8_t key_size = 16;
|
||||||
|
constexpr uint8_t tag_size = 16;
|
||||||
|
|
||||||
|
constexpr uint16_t max_entries = 101; // first is used for configuration
|
||||||
|
constexpr uint16_t max_length = 128;
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
// encrypted data
|
||||||
|
struct credential {
|
||||||
|
char title[max_length];
|
||||||
|
char username[max_length];
|
||||||
|
char password[max_length];
|
||||||
|
char totp_secret[max_length];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct vault {
|
||||||
|
uint16_t credential_count;
|
||||||
|
credential credentials[max_entries];
|
||||||
|
};
|
||||||
|
|
||||||
|
// unencrypted settings in header
|
||||||
|
struct device_settings {
|
||||||
|
uint8_t speaker_on_auto_lock; // speaker on bit, then auto lock bit
|
||||||
|
uint8_t input_mode;
|
||||||
|
uint8_t brightness;
|
||||||
|
uint8_t color_scheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
// file header
|
||||||
|
struct header {
|
||||||
|
char magic[magic_size];
|
||||||
|
uint8_t version;
|
||||||
|
|
||||||
|
uint32_t iterations;
|
||||||
|
uint8_t master_salt[salt_size];
|
||||||
|
uint8_t nonce[nonce_size];
|
||||||
|
|
||||||
|
device_settings settings;
|
||||||
|
|
||||||
|
uint32_t ciphertext_length;
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
// surface-level functions
|
||||||
|
bool get_key( // returns false if password is incorrect
|
||||||
|
const char* path,
|
||||||
|
const String& password,
|
||||||
|
uint8_t* key
|
||||||
|
);
|
||||||
|
|
||||||
|
bool init_vault( // create new vault using provided password
|
||||||
|
const char* path,
|
||||||
|
const String& password,
|
||||||
|
const device_settings& settings,
|
||||||
|
const vault& data
|
||||||
|
);
|
||||||
|
bool update_vault( // update vault data with new information
|
||||||
|
const char* path,
|
||||||
|
const uint8_t* key,
|
||||||
|
const device_settings& settings,
|
||||||
|
const vault& data
|
||||||
|
);
|
||||||
|
bool load_vault( // load existing vault to memory
|
||||||
|
const char* path,
|
||||||
|
const uint8_t* key,
|
||||||
|
device_settings& settings,
|
||||||
|
vault& data
|
||||||
|
);
|
||||||
|
|
||||||
|
bool update_config( // change unencrypted config without touching the vault data
|
||||||
|
const char* path,
|
||||||
|
const device_settings& settings
|
||||||
|
);
|
||||||
|
bool read_config( // read config without need to decrypt the entire file
|
||||||
|
const char* path,
|
||||||
|
device_settings& settings
|
||||||
|
);
|
||||||
|
|
||||||
|
bool get_salt( // because we need to give it to the PC if we're importing passwords
|
||||||
|
const char* path,
|
||||||
|
uint8_t* salt
|
||||||
|
);
|
||||||
|
bool replace_vault( // with import file
|
||||||
|
const char* path,
|
||||||
|
const device_settings& settings,
|
||||||
|
|
||||||
|
const uint8_t* key,
|
||||||
|
const uint8_t* salt,
|
||||||
|
|
||||||
|
const char* import_path
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env:m5stack-stamps3]
|
||||||
|
platform = espressif32
|
||||||
|
board = m5stack-stamps3
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
build_flags =
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=0
|
||||||
|
-D USE_NIMBLE
|
||||||
|
lib_deps =
|
||||||
|
m5stack/M5Unified@^0.2.17
|
||||||
|
m5stack/M5Cardputer@^1.1.1
|
||||||
|
m5stack/M5GFX@^0.2.24
|
||||||
|
arduino-libraries/NTPClient@^3.2.1
|
||||||
|
m5stack/M5Unit-RTC@^0.1.0
|
||||||
|
lucadentella/TOTP library@^1.1.0
|
||||||
|
h2zero/NimBLE-Arduino
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
# cryptography libs
|
||||||
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||||
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
|
||||||
|
# KeePass database support
|
||||||
|
from pykeepass import PyKeePass
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
# secure password input
|
||||||
|
from getpass import getpass
|
||||||
|
|
||||||
|
# data structure support
|
||||||
|
from ctypes import *
|
||||||
|
import struct
|
||||||
|
|
||||||
|
# communication
|
||||||
|
import serial
|
||||||
|
import serial.tools.list_ports
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# definitions
|
||||||
|
MAX_ENTRIES = 101
|
||||||
|
MAX_LENGTH = 128
|
||||||
|
|
||||||
|
SALT_SIZE = 16
|
||||||
|
ITER_SIZE = 4
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
PWROTOCOL_SERIAL_PREFIX = "/dev/cu"
|
||||||
|
elif sys.platform == "win32":
|
||||||
|
PWROTOCOL_SERIAL_PREFIX = "COM"
|
||||||
|
else:
|
||||||
|
PWROTOCOL_SERIAL_PREFIX = "/dev/ttyACM"
|
||||||
|
|
||||||
|
PWROTOCOL_IMPORT_MAGIC = "PWIMPORT"
|
||||||
|
PWROTOCOL_HANDSHAKE_SUCCESS = b"OK"
|
||||||
|
|
||||||
|
PWROTOCOL_PING = b'\x01'
|
||||||
|
PWROTOCOL_GET_SALT = b'\x02'
|
||||||
|
PWROTOCOL_GET_ITERATIONS = b'\x03'
|
||||||
|
PWROTOCOL_FILE_IMPORT = b'\x04'
|
||||||
|
PWROTOCOL_REQUEST_CONF = b'\x05'
|
||||||
|
|
||||||
|
PWROTOCOL_TIMEOUT_S = 10
|
||||||
|
PWROTOCOL_BAUDRATE = 115200
|
||||||
|
PWROTOCOL_CHUNK_SIZE = 64
|
||||||
|
|
||||||
|
class bcolors:
|
||||||
|
HEADER = '\033[95m'
|
||||||
|
OKBLUE = '\033[94m'
|
||||||
|
OKCYAN = '\033[96m'
|
||||||
|
OKGREEN = '\033[92m'
|
||||||
|
WARNING = '\033[93m'
|
||||||
|
FAIL = '\033[91m'
|
||||||
|
ENDC = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
UNDERLINE = '\033[4m'
|
||||||
|
|
||||||
|
class Credential(Structure):
|
||||||
|
_pack_ = 1
|
||||||
|
_layout_ = "ms"
|
||||||
|
_fields_ = [
|
||||||
|
("title", c_char * MAX_LENGTH),
|
||||||
|
("username", c_char * MAX_LENGTH),
|
||||||
|
("password", c_char * MAX_LENGTH),
|
||||||
|
("totp_secret", c_char * MAX_LENGTH),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Vault(Structure):
|
||||||
|
_pack_ = 1
|
||||||
|
_layout_ = "ms"
|
||||||
|
_fields_ = [
|
||||||
|
("credential_count", c_uint16),
|
||||||
|
("data", Credential * MAX_ENTRIES)
|
||||||
|
]
|
||||||
|
|
||||||
|
pwdstring = ''
|
||||||
|
vault = Vault()
|
||||||
|
|
||||||
|
vault.credential_count = 0
|
||||||
|
|
||||||
|
def smartSubstring(prefix, suffix, value):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
|
||||||
|
start = value.find(prefix)
|
||||||
|
if start == -1:
|
||||||
|
return None
|
||||||
|
start += len(prefix)
|
||||||
|
end = value.find(suffix, start)
|
||||||
|
|
||||||
|
return value[start:] if end == -1 else value[start:end]
|
||||||
|
|
||||||
|
def find_pwder():
|
||||||
|
print(bcolors.HEADER + "Looking for PWDer..." + bcolors.ENDC)
|
||||||
|
for port in serial.tools.list_ports.comports():
|
||||||
|
if PWROTOCOL_SERIAL_PREFIX not in port.device:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
print(f'Trying {port.device}')
|
||||||
|
pwder = serial.Serial(
|
||||||
|
port.device,
|
||||||
|
baudrate=PWROTOCOL_BAUDRATE,
|
||||||
|
timeout=PWROTOCOL_TIMEOUT_S
|
||||||
|
)
|
||||||
|
|
||||||
|
pwder.reset_input_buffer()
|
||||||
|
pwder.reset_output_buffer()
|
||||||
|
|
||||||
|
pwder.write(PWROTOCOL_PING)
|
||||||
|
reply = pwder.read(len(PWROTOCOL_HANDSHAKE_SUCCESS))
|
||||||
|
|
||||||
|
pwder.reset_input_buffer()
|
||||||
|
pwder.reset_output_buffer()
|
||||||
|
|
||||||
|
if reply == PWROTOCOL_HANDSHAKE_SUCCESS:
|
||||||
|
print(bcolors.OKGREEN + f'Found PWDer on {port.device}' + bcolors.ENDC)
|
||||||
|
return pwder
|
||||||
|
|
||||||
|
pwder.close()
|
||||||
|
|
||||||
|
except serial.SerialException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def send_chunked(ser: serial.Serial, data: bytes):
|
||||||
|
to_send = len(data)
|
||||||
|
sent = 0
|
||||||
|
|
||||||
|
while sent < to_send:
|
||||||
|
chunk = data[sent:sent + PWROTOCOL_CHUNK_SIZE]
|
||||||
|
ser.write(chunk)
|
||||||
|
ser.flush()
|
||||||
|
|
||||||
|
response = ser.read(len(PWROTOCOL_HANDSHAKE_SUCCESS))
|
||||||
|
|
||||||
|
if len(response) != len(PWROTOCOL_HANDSHAKE_SUCCESS):
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
if response != PWROTOCOL_HANDSHAKE_SUCCESS:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
sent += PWROTOCOL_CHUNK_SIZE
|
||||||
|
|
||||||
|
###
|
||||||
|
### Title screen
|
||||||
|
###
|
||||||
|
|
||||||
|
print(bcolors.HEADER + bcolors.BOLD + "=== PWDer KDBX Synchronization Tool ===" + bcolors.ENDC)
|
||||||
|
print("| Version 0.4")
|
||||||
|
print("| by @adamecki")
|
||||||
|
print()
|
||||||
|
|
||||||
|
### Input validation
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print(bcolors.FAIL + "E: KDBX file path not specified!" + bcolors.ENDC)
|
||||||
|
exit(1)
|
||||||
|
if os.path.exists(sys.argv[1]) == False:
|
||||||
|
print(bcolors.FAIL + "E: KDBX file specified does not exist!" + bcolors.ENDC)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
### Open kdbx
|
||||||
|
print(bcolors.HEADER + f'Opening KDBX file {sys.argv[1]}...' + bcolors.ENDC)
|
||||||
|
keepass_password = getpass()
|
||||||
|
|
||||||
|
try:
|
||||||
|
db = PyKeePass(sys.argv[1], password=keepass_password)
|
||||||
|
except:
|
||||||
|
print(bcolors.FAIL + "E: Error opening KDBX file. The password is incorrect or the specified file might be corrupt." + bcolors.ENDC)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
print(bcolors.OKGREEN + "KDBX file opened successfully." + bcolors.ENDC)
|
||||||
|
print()
|
||||||
|
|
||||||
|
### Find PWDer
|
||||||
|
print(bcolors.HEADER + "Attempting serial connection with PWDer." + bcolors.ENDC)
|
||||||
|
print("Please connect PWDer to your computer and then press Enter...", end='')
|
||||||
|
input()
|
||||||
|
print()
|
||||||
|
|
||||||
|
pwder = find_pwder()
|
||||||
|
if pwder is None:
|
||||||
|
print(bcolors.FAIL + "E: No PWDer found!" + bcolors.ENDC)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
### Get config
|
||||||
|
pwder.write(PWROTOCOL_REQUEST_CONF)
|
||||||
|
credential_bytes = pwder.read(sizeof(Credential))
|
||||||
|
pwder.reset_input_buffer()
|
||||||
|
pwder.reset_output_buffer()
|
||||||
|
credential = Credential.from_buffer_copy(credential_bytes)
|
||||||
|
vault.data[0].title = credential.title
|
||||||
|
vault.data[0].username = credential.username
|
||||||
|
vault.data[0].password = credential.password
|
||||||
|
vault.data[0].totp_secret = credential.totp_secret
|
||||||
|
|
||||||
|
### Get iteration number
|
||||||
|
pwder.write(PWROTOCOL_GET_ITERATIONS)
|
||||||
|
iterations_bytes = pwder.read(ITER_SIZE)
|
||||||
|
pwder.reset_input_buffer()
|
||||||
|
pwder.reset_output_buffer()
|
||||||
|
iterations = struct.unpack("<I", iterations_bytes)[0]
|
||||||
|
|
||||||
|
### Get salt
|
||||||
|
pwder.write(PWROTOCOL_GET_SALT)
|
||||||
|
salt = pwder.read(SALT_SIZE)
|
||||||
|
pwder.reset_input_buffer()
|
||||||
|
pwder.reset_output_buffer()
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
### Derive key
|
||||||
|
print(bcolors.HEADER + "Please enter your PWDer password." + bcolors.ENDC)
|
||||||
|
print(bcolors.WARNING + "The password is not checked. Entering an incorrect password will result in an import failure." + bcolors.ENDC)
|
||||||
|
|
||||||
|
pwder_password = getpass().encode("utf-8")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(bcolors.HEADER + "Deriving AES128 key... " + bcolors.ENDC, end='')
|
||||||
|
|
||||||
|
kdf = PBKDF2HMAC(
|
||||||
|
algorithm=hashes.SHA256(),
|
||||||
|
length=16,
|
||||||
|
salt=salt,
|
||||||
|
iterations=iterations,
|
||||||
|
)
|
||||||
|
|
||||||
|
key = kdf.derive(pwder_password)
|
||||||
|
|
||||||
|
print(bcolors.OKGREEN + "OK" + bcolors.ENDC)
|
||||||
|
print()
|
||||||
|
|
||||||
|
### Fill vault with data
|
||||||
|
print(bcolors.HEADER + "Reading KDBX data... " + bcolors.ENDC, end='')
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
for entry in db.entries:
|
||||||
|
if i < 100:
|
||||||
|
vault.credential_count += 1
|
||||||
|
|
||||||
|
if not entry.title:
|
||||||
|
vault.data[i+1].title = bytes("", 'utf-8')
|
||||||
|
else:
|
||||||
|
vault.data[i+1].title = bytes(entry.title, 'utf-8')
|
||||||
|
|
||||||
|
if not entry.username:
|
||||||
|
vault.data[i+1].username = bytes("", 'utf-8')
|
||||||
|
else:
|
||||||
|
vault.data[i+1].username = bytes(entry.username, 'utf-8')
|
||||||
|
|
||||||
|
if not entry.password:
|
||||||
|
vault.data[i+1].password = bytes("", 'utf-8')
|
||||||
|
else:
|
||||||
|
vault.data[i+1].password = bytes(entry.password, 'utf-8')
|
||||||
|
|
||||||
|
otp = smartSubstring("secret=", "&", f'{entry.otp}')
|
||||||
|
if otp:
|
||||||
|
otp = unquote(otp)
|
||||||
|
vault.data[i+1].totp_secret = bytes(otp, 'utf-8')
|
||||||
|
else:
|
||||||
|
vault.data[i+1].totp_secret = bytes("", 'utf-8')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
print(bcolors.OKGREEN + "OK" + bcolors.ENDC)
|
||||||
|
print()
|
||||||
|
|
||||||
|
### Encrypt vault data
|
||||||
|
print(bcolors.HEADER + "Encrypting data in PWDer-friendly format... " + bcolors.ENDC, end='')
|
||||||
|
|
||||||
|
plaintext = bytes(vault)
|
||||||
|
nonce = os.urandom(12)
|
||||||
|
|
||||||
|
aes = AESGCM(key)
|
||||||
|
encrypted = aes.encrypt(
|
||||||
|
nonce,
|
||||||
|
plaintext,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
ciphertext = encrypted[:-16]
|
||||||
|
tag = encrypted[-16:]
|
||||||
|
|
||||||
|
print(bcolors.OKGREEN + "OK" + bcolors.ENDC)
|
||||||
|
print()
|
||||||
|
|
||||||
|
### Send to PWDer
|
||||||
|
print(bcolors.HEADER + "Sending data to PWDer... " + bcolors.ENDC, end='')
|
||||||
|
|
||||||
|
pwder.write(PWROTOCOL_FILE_IMPORT)
|
||||||
|
pwder.read(2)
|
||||||
|
|
||||||
|
pwder.write(b"PWIMPORT")
|
||||||
|
pwder.read(2)
|
||||||
|
|
||||||
|
pwder.write(nonce)
|
||||||
|
pwder.read(2)
|
||||||
|
|
||||||
|
pwder.write(struct.pack("<I", len(ciphertext)))
|
||||||
|
pwder.read(2)
|
||||||
|
|
||||||
|
send_chunked(pwder, ciphertext)
|
||||||
|
|
||||||
|
pwder.write(tag)
|
||||||
|
pwder.read(2)
|
||||||
|
|
||||||
|
print(bcolors.OKGREEN + "OK" + bcolors.ENDC)
|
||||||
|
print()
|
||||||
|
print(bcolors.OKGREEN + "Vault sent to PWDer. Follow the instructions on PWDer's screen. Goodbye." + bcolors.ENDC)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#include "globals.h"
|
||||||
|
#include "gui.h"
|
||||||
|
|
||||||
|
extern pvault::vault entries;
|
||||||
|
extern pvault::device_settings configuration;
|
||||||
|
extern uint8_t aes_key[pvault::key_size];
|
||||||
|
|
||||||
|
extern USBCDC USBSerialDevice;
|
||||||
|
|
||||||
|
void init_new_vault() {
|
||||||
|
pvault::credential init_cred{};
|
||||||
|
|
||||||
|
// first credental is used for storing encrypted configuration
|
||||||
|
strncpy(init_cred.title, DEFAULT_SSID, sizeof(init_cred.title));
|
||||||
|
strncpy(init_cred.username, DEFAULT_WIFI_PASSWORD, sizeof(init_cred.username));
|
||||||
|
strncpy(init_cred.password, DEFAULT_SYNCHOST, sizeof(init_cred.password));
|
||||||
|
strncpy(init_cred.totp_secret, DEFAULT_SYNCPORT, sizeof(init_cred.totp_secret));
|
||||||
|
|
||||||
|
entries.credentials[0] = init_cred;
|
||||||
|
|
||||||
|
// second and next are used for storing actual user data
|
||||||
|
strncpy(init_cred.title, SAMPLE_ENTRY, sizeof(init_cred.title));
|
||||||
|
strncpy(init_cred.username, SAMPLE_USERNAME, sizeof(init_cred.username));
|
||||||
|
strncpy(init_cred.password, SAMPLE_PASSWORD, sizeof(init_cred.password));
|
||||||
|
strncpy(init_cred.totp_secret, "", sizeof(init_cred.totp_secret));
|
||||||
|
|
||||||
|
entries.credentials[1] = init_cred;
|
||||||
|
entries.credential_count = 1;
|
||||||
|
|
||||||
|
configuration.color_scheme = 0;
|
||||||
|
configuration.input_mode = 2;
|
||||||
|
configuration.speaker_on_auto_lock = 2;
|
||||||
|
configuration.brightness = 0xFF;
|
||||||
|
|
||||||
|
pvault::init_vault(VAULT_PATH, DEFAULT_PASSWORD, configuration, entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
void export_vault() {
|
||||||
|
String export_save_string = "";
|
||||||
|
for(int i = 1; i <= entries.credential_count; i++) {
|
||||||
|
export_save_string += String(entries.credentials[i].title) + String('\n');
|
||||||
|
export_save_string += String(entries.credentials[i].username) + String('\n');
|
||||||
|
export_save_string += String(entries.credentials[i].password) + String('\n');
|
||||||
|
export_save_string += String(entries.credentials[i].totp_secret) + String('\n');
|
||||||
|
if(i < entries.credential_count) {
|
||||||
|
export_save_string += String('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(SD.exists(EXPORT_FILE_PATH)) { SD.remove(EXPORT_FILE_PATH); }
|
||||||
|
|
||||||
|
File export_file = SD.open(EXPORT_FILE_PATH, FILE_WRITE);
|
||||||
|
export_file.print(export_save_string);
|
||||||
|
export_file.close();
|
||||||
|
}
|
||||||
@@ -0,0 +1,663 @@
|
|||||||
|
#include "globals.h"
|
||||||
|
|
||||||
|
#include "gui.h"
|
||||||
|
#include "icons.h"
|
||||||
|
#include "time_operations.h"
|
||||||
|
#include "keyboard_bridge.h"
|
||||||
|
|
||||||
|
extern M5Canvas canvas;
|
||||||
|
extern Unit_RTC RTC;
|
||||||
|
extern rtc_time_type rtc_time;
|
||||||
|
|
||||||
|
extern bool network_available;
|
||||||
|
extern bool rtc_available;
|
||||||
|
extern bool totp_available;
|
||||||
|
extern char totp_buffer[7];
|
||||||
|
extern int8_t last_battery_percentage;
|
||||||
|
extern uint8_t* motd_number;
|
||||||
|
|
||||||
|
extern int device_mode;
|
||||||
|
extern int mode2_page;
|
||||||
|
extern int mode3_page;
|
||||||
|
extern int mode4_page;
|
||||||
|
extern int mode5_page;
|
||||||
|
|
||||||
|
extern int mode0_selection;
|
||||||
|
|
||||||
|
extern String mode1_passwordinput;
|
||||||
|
|
||||||
|
extern String mode3_tempssid;
|
||||||
|
extern String mode3_tempwpwd;
|
||||||
|
extern String mode3_tempaddr;
|
||||||
|
extern String mode3_tempport;
|
||||||
|
extern String mode3_tempdpwd;
|
||||||
|
extern int mode3_tempbrightness;
|
||||||
|
|
||||||
|
extern bool mode7_show_results;
|
||||||
|
extern String mode7_query;
|
||||||
|
extern int mode7_matches;
|
||||||
|
extern int mode7_matchindex[100];
|
||||||
|
extern int mode7_index;
|
||||||
|
|
||||||
|
extern pvault::vault entries;
|
||||||
|
extern pvault::device_settings configuration;
|
||||||
|
|
||||||
|
extern const pwder_style::color_scheme pwcolors[7];
|
||||||
|
|
||||||
|
void splash_screen() {
|
||||||
|
canvas.clear(pwcolors[configuration.color_scheme].background_color);
|
||||||
|
|
||||||
|
canvas.fillRect(0, 119, 240, 18, lgfx::color565(0xB3, 0xB3, 0xB3));
|
||||||
|
|
||||||
|
canvas.setTextColor(TFT_BLACK);
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
canvas.setTextSize(1);
|
||||||
|
canvas.drawString("@floriano", 168, 119);
|
||||||
|
|
||||||
|
push_icon(me, 0, 40, 3);
|
||||||
|
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.setTextSize(4);
|
||||||
|
canvas.setTextDatum(top_right);
|
||||||
|
canvas.drawString("PWDer", 232, 12);
|
||||||
|
|
||||||
|
canvas.setTextSize(1);
|
||||||
|
canvas.drawString(PWDER_VERSION, 224, 68);
|
||||||
|
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
canvas.drawString(motd[motd_number[0]], M5Cardputer.Display.width() / 2, 4);
|
||||||
|
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void splash_screen_create_progressbar() {
|
||||||
|
canvas.drawRect(108, 90, 116, 18, pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void splash_screen_update_progressbar_percentage(int progress) {
|
||||||
|
if(progress < 0) {
|
||||||
|
progress = 0;
|
||||||
|
}
|
||||||
|
if(progress > 100) {
|
||||||
|
progress = 100;
|
||||||
|
}
|
||||||
|
progress = map(progress, 0, 100, 0, 112);
|
||||||
|
canvas.fillRect(110, 92, progress, 14, pwcolors[configuration.color_scheme].statusbar_color);
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void save_screenshot_bmp() {
|
||||||
|
const int w = 240;
|
||||||
|
const int h = 136;
|
||||||
|
uint32_t size = w * h * 3;
|
||||||
|
|
||||||
|
RGBColor* row = new RGBColor[240];
|
||||||
|
|
||||||
|
int padding = ((w * 3 + 3) & ~3) - (w * 3);
|
||||||
|
uint8_t padding_bytes[3] = {0, 0, 0};
|
||||||
|
|
||||||
|
bmp_header hdr;
|
||||||
|
hdr.bf_size = size + 54;
|
||||||
|
hdr.bi_width = w;
|
||||||
|
hdr.bi_height = -h;
|
||||||
|
|
||||||
|
int screenshot_number = 1;
|
||||||
|
String filename = "/pwscreenshot" + String(screenshot_number) + ".bmp";
|
||||||
|
while(SD.exists(filename)) {
|
||||||
|
screenshot_number += 1;
|
||||||
|
filename = "/pwscreenshot" + String(screenshot_number) + ".bmp";
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = SD.open(filename, FILE_WRITE);
|
||||||
|
if(!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file.write((uint8_t*)&hdr, sizeof(hdr));
|
||||||
|
|
||||||
|
for(int i = 0; i < h; i++) {
|
||||||
|
canvas.readRectRGB(0, i, w, 1, row);
|
||||||
|
|
||||||
|
for(int j = 0; j < w; j++) {
|
||||||
|
file.write(row[j].b);
|
||||||
|
file.write(row[j].g);
|
||||||
|
file.write(row[j].r);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(padding > 0) {
|
||||||
|
file.write(padding_bytes, padding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(row);
|
||||||
|
file.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void push_icon(const pwder_style::icon32 &icon, int xoffset, int yoffset, int scale) {
|
||||||
|
canvas.fillRect(xoffset, yoffset, 32 * scale, 32 * scale, pwcolors[configuration.color_scheme].background_color);
|
||||||
|
|
||||||
|
for (int y = 0; y < 32; y++) {
|
||||||
|
for (int x = 0; x < 32; x++) {
|
||||||
|
int index = ((y * 32) + x) * 3;
|
||||||
|
|
||||||
|
uint8_t r = icon.bitmap[index];
|
||||||
|
uint8_t g = icon.bitmap[index + 1];
|
||||||
|
uint8_t b = icon.bitmap[index + 2];
|
||||||
|
|
||||||
|
// xs - X scaled, ys - Y scaled
|
||||||
|
for (int xs = 1; xs <= scale; xs++) {
|
||||||
|
for (int ys = 1; ys <= scale; ys++) {
|
||||||
|
if(!icon.mask[index / 3]) {
|
||||||
|
canvas.drawPixel((x * scale) + xoffset + xs, (y * scale) + yoffset + ys, lgfx::v1::color888(r, g, b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void no_sdcard_crash_screen() {
|
||||||
|
canvas.fillRect(0, 0, M5Cardputer.Display.width(), M5Cardputer.Display.height(), pwcolors[configuration.color_scheme].background_color);
|
||||||
|
canvas.fillRect(0, 40, M5Cardputer.Display.width(), M5Cardputer.Display.height() - 80, pwcolors[configuration.color_scheme].border_color);
|
||||||
|
canvas.fillRect(0, 44, M5Cardputer.Display.width(), M5Cardputer.Display.height() - 88, pwcolors[configuration.color_scheme].foreground_color);
|
||||||
|
|
||||||
|
// icon
|
||||||
|
push_icon(error, 4, 4, 1);
|
||||||
|
|
||||||
|
// caption
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
canvas.drawString(SDCARD_NOT_FOUND_TITLE, 40, 20);
|
||||||
|
|
||||||
|
// content
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
canvas.drawString(SDCARD_NOT_FOUND_DESCRIPTION, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void connection_init_error() {
|
||||||
|
canvas.fillRect(0, 0, M5Cardputer.Display.width(), M5Cardputer.Display.height(), pwcolors[configuration.color_scheme].background_color);
|
||||||
|
canvas.fillRect(0, 40, M5Cardputer.Display.width(), M5Cardputer.Display.height() - 80, pwcolors[configuration.color_scheme].border_color);
|
||||||
|
canvas.fillRect(0, 44, M5Cardputer.Display.width(), M5Cardputer.Display.height() - 88, pwcolors[configuration.color_scheme].foreground_color);
|
||||||
|
|
||||||
|
// icon
|
||||||
|
push_icon(error, 4, 4, 1);
|
||||||
|
|
||||||
|
// caption
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
canvas.drawString(CONNECTION_LIMIT_TITLE, 40, 20);
|
||||||
|
|
||||||
|
// content
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
canvas.drawString(CONNECTION_LIMIT_DESCRIPTION, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void password_import_screen() {
|
||||||
|
canvas.fillRect(0, 0, M5Cardputer.Display.width(), M5Cardputer.Display.height(), pwcolors[configuration.color_scheme].background_color);
|
||||||
|
canvas.fillRect(0, 40, M5Cardputer.Display.width(), M5Cardputer.Display.height() - 80, pwcolors[configuration.color_scheme].border_color);
|
||||||
|
canvas.fillRect(0, 44, M5Cardputer.Display.width(), M5Cardputer.Display.height() - 88, pwcolors[configuration.color_scheme].foreground_color);
|
||||||
|
|
||||||
|
// icon
|
||||||
|
push_icon(options, 4, 4, 1);
|
||||||
|
|
||||||
|
// caption
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
canvas.drawString(PWIMPORT_TITLEBAR, 40, 20);
|
||||||
|
|
||||||
|
// content
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
canvas.drawString(PWIMPORT_REPLACE_PASSWORDS, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString("[ Y / N ]", M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_ui() {
|
||||||
|
String mode1_asterisks = ""; // password mask, also used in mode 3 (options) to hide Wi-Fi and device password
|
||||||
|
String enumerator = "";
|
||||||
|
|
||||||
|
// background
|
||||||
|
canvas.fillRect(0, 0, M5Cardputer.Display.width(), M5Cardputer.Display.height(), pwcolors[configuration.color_scheme].background_color);
|
||||||
|
canvas.fillRect(0, 40, M5Cardputer.Display.width(), M5Cardputer.Display.height() - 80, pwcolors[configuration.color_scheme].border_color);
|
||||||
|
canvas.fillRect(0, 44, M5Cardputer.Display.width(), M5Cardputer.Display.height() - 88, pwcolors[configuration.color_scheme].foreground_color);
|
||||||
|
|
||||||
|
switch (device_mode) {
|
||||||
|
case 0:
|
||||||
|
// icon
|
||||||
|
push_icon(key, 4, 4, 1);
|
||||||
|
|
||||||
|
// caption
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.drawString(PASSWORD_MANAGER_TITLEBAR, 40, 20);
|
||||||
|
|
||||||
|
// content
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
if (M5Cardputer.Keyboard.isKeyPressed('v')) {
|
||||||
|
if((network_available || rtc_available) && totp_available) {
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
canvas.drawString(String(entries.credentials[mode0_selection].password), M5Cardputer.Display.width() / 2, 50);
|
||||||
|
|
||||||
|
generate_totp(String(entries.credentials[mode0_selection].totp_secret));
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].statusbar_color);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(String(totp_buffer), M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
totp_buffer[6] = '\0';
|
||||||
|
} else {
|
||||||
|
canvas.setTextDatum(middle_center);
|
||||||
|
canvas.drawString(String(entries.credentials[mode0_selection].password), M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() / 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
if ((network_available || rtc_available) && totp_available) {
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].statusbar_color);
|
||||||
|
}
|
||||||
|
canvas.drawString(String(entries.credentials[mode0_selection].title), M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(String(entries.credentials[mode0_selection].username), M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
// icon
|
||||||
|
push_icon(padlock, 4, 4, 1);
|
||||||
|
|
||||||
|
// caption
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
canvas.drawString(LOGIN_TITLEBAR, 40, 20);
|
||||||
|
|
||||||
|
// content
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
canvas.setTextDatum(middle_center);
|
||||||
|
mode1_asterisks = "";
|
||||||
|
for (int i = 0; i < mode1_passwordinput.length(); i++) {
|
||||||
|
mode1_asterisks += "*";
|
||||||
|
}
|
||||||
|
canvas.drawString(mode1_asterisks + "_", M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() / 2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// icon
|
||||||
|
push_icon(help, 4, 4, 1);
|
||||||
|
|
||||||
|
// caption
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
canvas.drawString(HANDBOOK_TITLEBAR, 40, 20);
|
||||||
|
|
||||||
|
// content
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
canvas.drawString(handbook[mode2_page * 2], M5Cardputer.Display.width() / 2, 50);
|
||||||
|
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(handbook[(mode2_page * 2) + 1], M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
// icon
|
||||||
|
push_icon(options, 4, 4, 1);
|
||||||
|
|
||||||
|
// caption
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
canvas.drawString(OPTIONS_TITLEBAR, 40, 20);
|
||||||
|
|
||||||
|
// content
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
switch (mode3_page) {
|
||||||
|
case 0:
|
||||||
|
canvas.drawString(OPTIONS_BRIGHTNESS, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(String(map(mode3_tempbrightness, 0x00, 0xFF, 0, 100)) + OPTIONS_BRIGHTNESS_PERCENTAGE, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
canvas.setTextDatum(middle_center);
|
||||||
|
switch (configuration.input_mode) {
|
||||||
|
case 0:
|
||||||
|
canvas.drawString(OPTIONS_DEFAULT_USERNAME, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() / 2);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
canvas.drawString(OPTIONS_DEFAULT_PASSWORD, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() / 2);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
canvas.drawString(OPTIONS_DEFAULT_FULL, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() / 2);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
canvas.setTextDatum(middle_center);
|
||||||
|
if(configuration.speaker_on_auto_lock == 2 || configuration.speaker_on_auto_lock == 3) {
|
||||||
|
canvas.drawString(OPTIONS_AUTOLOCK_ON, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() / 2);
|
||||||
|
} else {
|
||||||
|
canvas.drawString(OPTIONS_AUTOLOCK_OFF, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() / 2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
canvas.drawString(OPTIONS_COLOR_SCHEME, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(pwcolors[configuration.color_scheme].name, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
mode1_asterisks = "";
|
||||||
|
for (int i = 0; i < mode3_tempdpwd.length(); i++) {
|
||||||
|
mode1_asterisks += "*";
|
||||||
|
}
|
||||||
|
canvas.drawString(OPTIONS_DEVICE_PASSWORD, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(mode1_asterisks + "_", M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
if (rtc_available) {
|
||||||
|
RTC.getTime(&rtc_time);
|
||||||
|
String nice_hours = "";
|
||||||
|
String nice_minutes = "";
|
||||||
|
if (rtc_time.Hours < 10) {
|
||||||
|
nice_hours = "0" + String(rtc_time.Hours);
|
||||||
|
} else {
|
||||||
|
nice_hours = String(rtc_time.Hours);
|
||||||
|
}
|
||||||
|
if (rtc_time.Minutes < 10) {
|
||||||
|
nice_minutes = "0" + String(rtc_time.Minutes);
|
||||||
|
} else {
|
||||||
|
nice_minutes = String(rtc_time.Minutes);
|
||||||
|
}
|
||||||
|
canvas.drawString(OPTIONS_RTC_TIME + nice_hours + ":" + nice_minutes + " UTC", M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(OPTIONS_RTC_NTPSYNC, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
} else {
|
||||||
|
canvas.drawString(OPTIONS_RTC_NTPSYNC_RTC_UNAVAILABLE, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
canvas.drawString(OPTIONS_SSID, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(mode3_tempssid + "_", M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
mode1_asterisks = "";
|
||||||
|
for (int i = 0; i < mode3_tempwpwd.length(); i++) {
|
||||||
|
mode1_asterisks += "*";
|
||||||
|
}
|
||||||
|
canvas.drawString(OPTIONS_WIFI_PASSWORD, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(mode1_asterisks + "_", M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
// icon
|
||||||
|
switch (mode4_page) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
push_icon(drum, 4, 4, 1);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
push_icon(ok, 4, 4, 1);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
case 6:
|
||||||
|
push_icon(error, 4, 4, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// caption
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
canvas.drawString(SYNC_TITLEBAR, 40, 20);
|
||||||
|
|
||||||
|
// content
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
switch (mode4_page) {
|
||||||
|
case 0:
|
||||||
|
canvas.drawString(SYNC_WIFI_CONNECTING_PHASE, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString("[ 1 / 3 ]", M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
canvas.drawString(SYNC_SERVER_CONNECTING_PHASE, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString("[ 2 / 3 ]", M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
canvas.drawString(SYNC_DOWNLOAD_PHASE, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString("[ 3 / 3 ]", M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
canvas.drawString(SYNC_OK, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(SYNC_RETURN, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
canvas.drawString(SYNC_ERR, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(SYNC_ERR_DESCRIPTION_WIFI, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
canvas.drawString(SYNC_ERR, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(SYNC_ERR_DESCRIPTION_SERVER, M5Cardputer.Display.width() / 2,
|
||||||
|
M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
canvas.drawString(SYNC_ERR, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(SYNC_ERR_DESCRIPTION_FILE, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
// icon
|
||||||
|
push_icon(me, 4, 4, 1);
|
||||||
|
|
||||||
|
// caption
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
canvas.drawString(CREDITS_TITLEBAR, 40, 20);
|
||||||
|
|
||||||
|
// content
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
switch (mode5_page) {
|
||||||
|
case 0:
|
||||||
|
canvas.drawString(CREDITS_PWDER, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
canvas.drawString(CREDITS_WEBSITE, M5Cardputer.Display.width() / 2, 50);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].statusbar_color);
|
||||||
|
switch (mode5_page) {
|
||||||
|
case 0:
|
||||||
|
canvas.drawString(CREDITS_PWDER_GITHUB, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
canvas.drawString(CREDITS_WEBSITE_LINK, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 7:
|
||||||
|
// icon
|
||||||
|
push_icon(search, 4, 4, 1);
|
||||||
|
|
||||||
|
// caption
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
if (mode7_show_results) {
|
||||||
|
canvas.drawString(mode7_query, 40, 20);
|
||||||
|
} else {
|
||||||
|
canvas.drawString(SEARCH_TITLEBAR, 40, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
// content
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
if (mode7_show_results) {
|
||||||
|
if (M5Cardputer.Keyboard.isKeyPressed('v') && mode7_matches > 0) {
|
||||||
|
// password and otp preview
|
||||||
|
if((network_available || rtc_available) && totp_available) {
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
canvas.drawString(String(entries.credentials[mode7_matchindex[mode7_index]].password), M5Cardputer.Display.width() / 2, 50);
|
||||||
|
|
||||||
|
generate_totp(String(entries.credentials[mode7_matchindex[mode7_index]].totp_secret));
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].statusbar_color);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(String(totp_buffer), M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
totp_buffer[6] = '\0';
|
||||||
|
} else {
|
||||||
|
canvas.setTextDatum(middle_center);
|
||||||
|
canvas.drawString(String(entries.credentials[mode7_matchindex[mode7_index]].password), M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() / 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mode7_matches > 0) {
|
||||||
|
// regular display
|
||||||
|
canvas.setTextDatum(top_center);
|
||||||
|
if ((network_available || rtc_available) && totp_available) {
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].statusbar_color);
|
||||||
|
}
|
||||||
|
canvas.drawString(String(entries.credentials[mode7_matchindex[mode7_index]].title), M5Cardputer.Display.width() / 2, 50);
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].foreground_text_color);
|
||||||
|
canvas.setTextDatum(bottom_center);
|
||||||
|
canvas.drawString(String(entries.credentials[mode7_matchindex[mode7_index]].username), M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() - 50);
|
||||||
|
} else {
|
||||||
|
// no results
|
||||||
|
canvas.setTextDatum(middle_center);
|
||||||
|
canvas.drawString(SEARCH_NO_RESULTS, M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canvas.setTextDatum(middle_center);
|
||||||
|
canvas.drawString(mode7_query + "_", M5Cardputer.Display.width() / 2, M5Cardputer.Display.height() / 2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the mostly unchanging rest of the UI
|
||||||
|
if (!(configuration.speaker_on_auto_lock & 1) && device_mode != 1) {
|
||||||
|
push_icon(loudspeaker, M5Cardputer.Display.width() - 36, M5Cardputer.Display.height() - 36, 1);
|
||||||
|
} else if (configuration.speaker_on_auto_lock & 1 && device_mode != 1) {
|
||||||
|
push_icon(loudspeaker_unmuted, M5Cardputer.Display.width() - 36, M5Cardputer.Display.height() - 36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.setTextColor(pwcolors[configuration.color_scheme].background_text_color);
|
||||||
|
canvas.setTextDatum(middle_right);
|
||||||
|
|
||||||
|
// icon placement
|
||||||
|
int iconx[3] = { 204, 168, 132 };
|
||||||
|
bool iconx_used[3] = { false, false, false };
|
||||||
|
|
||||||
|
if(ble_keyboard_ready()) {
|
||||||
|
for(int i = 0; i < 3; i++) {
|
||||||
|
if(!iconx_used[i]) {
|
||||||
|
iconx_used[i] = true;
|
||||||
|
push_icon(bluetooth, iconx[i], 4, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(network_available) {
|
||||||
|
for(int i = 0; i < 3; i++) {
|
||||||
|
if(!iconx_used[i]) {
|
||||||
|
iconx_used[i] = true;
|
||||||
|
push_icon(network, iconx[i], 4, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(rtc_available) {
|
||||||
|
for(int i = 0; i < 3; i++) {
|
||||||
|
if(!iconx_used[i]) {
|
||||||
|
iconx_used[i] = true;
|
||||||
|
push_icon(clockicon, iconx[i], 4, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device_mode != 1) {
|
||||||
|
push_icon(help, M5Cardputer.Display.width() - 108, M5Cardputer.Display.height() - 36, 1);
|
||||||
|
if (!(device_mode == 7 && mode7_show_results == false)) {
|
||||||
|
canvas.drawString("M ", M5Cardputer.Display.width() - 40, M5Cardputer.Display.height() - 20);
|
||||||
|
canvas.drawString("H ", M5Cardputer.Display.width() - 112, M5Cardputer.Display.height() - 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.setTextDatum(middle_left);
|
||||||
|
canvas.drawString(String(last_battery_percentage) + "%", 40, M5Cardputer.Display.height() - 20);
|
||||||
|
push_icon(battery, 4, M5Cardputer.Display.height() - 36, 1);
|
||||||
|
|
||||||
|
// vault usage
|
||||||
|
int vault_usage_statusbar_width = map(entries.credential_count, 0, pvault::max_entries - 1, 0, M5Cardputer.Display.width());
|
||||||
|
canvas.fillRect(0, 40, vault_usage_statusbar_width, 4, pwcolors[configuration.color_scheme].statusbar_color);
|
||||||
|
|
||||||
|
// current position or progress
|
||||||
|
int statusbar_left = 0;
|
||||||
|
int statusbar_width = 0;
|
||||||
|
|
||||||
|
switch(device_mode) {
|
||||||
|
case 0:
|
||||||
|
if(entries.credential_count != 0) {
|
||||||
|
statusbar_width = M5Cardputer.Display.width() / entries.credential_count;
|
||||||
|
statusbar_left = map(mode0_selection, 1, entries.credential_count, 0, M5Cardputer.Display.width() - statusbar_width);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
statusbar_width = M5Cardputer.Display.width() / 11;
|
||||||
|
statusbar_left = map(mode2_page, 0, 10, 0, M5Cardputer.Display.width() - statusbar_width);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
statusbar_width = M5Cardputer.Display.width() / MODE3_PAGES_NUMBER;
|
||||||
|
statusbar_left = map(mode3_page, 0, MODE3_PAGES_NUMBER - 1, 0, M5Cardputer.Display.width() - statusbar_width);
|
||||||
|
break;
|
||||||
|
// case 4:
|
||||||
|
// break;
|
||||||
|
case 5:
|
||||||
|
statusbar_width = M5Cardputer.Display.width() / 2;
|
||||||
|
statusbar_left = map(mode5_page, 0, 1, 0, M5Cardputer.Display.width() - statusbar_width);
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
if(mode7_matches != 0) {
|
||||||
|
statusbar_width = M5Cardputer.Display.width() / mode7_matches;
|
||||||
|
statusbar_left = map(mode7_index, 0, mode7_matches - 1, 0, M5Cardputer.Display.width() - statusbar_width);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusbar_width += 1; // bro
|
||||||
|
canvas.fillRect(statusbar_left, M5Cardputer.Display.height() - 44, statusbar_width, 4, pwcolors[configuration.color_scheme].statusbar_color);
|
||||||
|
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <M5Cardputer.h>
|
||||||
|
#include "keyboard_bridge.h"
|
||||||
|
#include "gui.h"
|
||||||
|
#include "network_operations.h"
|
||||||
|
#include "asciimap.h"
|
||||||
|
|
||||||
|
#include <BleCompositeHID.h>
|
||||||
|
#include <KeyboardDevice.h>
|
||||||
|
|
||||||
|
BleCompositeHID compositeHID("Cardputer", "M5Stack", 100);
|
||||||
|
BLEHostConfiguration bleHostConfig;
|
||||||
|
KeyboardDevice* bleKeyboard;
|
||||||
|
extern bool network_initialized;
|
||||||
|
extern bool bluetooth_initialized;
|
||||||
|
|
||||||
|
static uint8_t to_ble_code(special_key k) {
|
||||||
|
switch(k) {
|
||||||
|
case special_key::RETURN: return KEY_ENTER;
|
||||||
|
case special_key::TAB: return KEY_TAB;
|
||||||
|
default: return KEY_TAB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_keyboard_init() {
|
||||||
|
if(network_initialized) {
|
||||||
|
connection_init_error();
|
||||||
|
delay(3000);
|
||||||
|
draw_ui();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bleHostConfig.setHidType(HID_KEYBOARD);
|
||||||
|
|
||||||
|
bleKeyboard = new KeyboardDevice();
|
||||||
|
compositeHID.addDevice(bleKeyboard);
|
||||||
|
compositeHID.begin(bleHostConfig);
|
||||||
|
|
||||||
|
bluetooth_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_keyboard_update_battery_level() {
|
||||||
|
compositeHID.setBatteryLevel(M5.Power.getBatteryLevel());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ble_keyboard_ready() {
|
||||||
|
return compositeHID.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_send_key(special_key k) {
|
||||||
|
bleKeyboard->keyPress(to_ble_code(k));
|
||||||
|
delay(25);
|
||||||
|
bleKeyboard->keyRelease(to_ble_code(k));
|
||||||
|
delay(25);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ble_send_key(char c) {
|
||||||
|
uint8_t ascii = (uint8_t)c;
|
||||||
|
if(ascii > 127) { return; }
|
||||||
|
|
||||||
|
keymap map = ascii_to_hid[ascii];
|
||||||
|
if(map.usage_id == 0) { return; }
|
||||||
|
|
||||||
|
if (map.shift) {
|
||||||
|
bleKeyboard->modifierKeyPress(KEY_MOD_LSHIFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
bleKeyboard->keyPress(map.usage_id);
|
||||||
|
delay(25);
|
||||||
|
|
||||||
|
bleKeyboard->keyRelease(map.usage_id);
|
||||||
|
delay(25);
|
||||||
|
|
||||||
|
if (map.shift) {
|
||||||
|
bleKeyboard->modifierKeyRelease(KEY_MOD_LSHIFT);
|
||||||
|
delay(25);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include "keyboard_bridge.h"
|
||||||
|
#include <USBHIDKeyboard.h>
|
||||||
|
|
||||||
|
USBHIDKeyboard Keyboard;
|
||||||
|
|
||||||
|
static uint8_t to_usb_code(special_key k) {
|
||||||
|
switch(k) {
|
||||||
|
case special_key::RETURN: return KEY_RETURN;
|
||||||
|
case special_key::TAB: return KEY_TAB;
|
||||||
|
default: return KEY_TAB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void usb_keyboard_init() {
|
||||||
|
Keyboard.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void usb_send_key(special_key k) {
|
||||||
|
Keyboard.press(to_usb_code(k));
|
||||||
|
delay(25);
|
||||||
|
Keyboard.releaseAll();
|
||||||
|
delay(25);
|
||||||
|
}
|
||||||
|
|
||||||
|
void usb_send_key(char c) {
|
||||||
|
Keyboard.press(c);
|
||||||
|
delay(25);
|
||||||
|
Keyboard.releaseAll();
|
||||||
|
delay(25);
|
||||||
|
}
|
||||||
@@ -0,0 +1,693 @@
|
|||||||
|
#include "globals.h"
|
||||||
|
|
||||||
|
#include "file_operations.h"
|
||||||
|
#include "gui.h"
|
||||||
|
#include "sfx.h"
|
||||||
|
#include "icons.h"
|
||||||
|
#include "network_operations.h"
|
||||||
|
#include "keyboard_bridge.h"
|
||||||
|
#include "time_operations.h"
|
||||||
|
|
||||||
|
#define BASIC_BRIGHTNESS 0xFF
|
||||||
|
extern const int color_schemes_number;
|
||||||
|
|
||||||
|
extern M5Canvas canvas;
|
||||||
|
extern NTPClient timeClient;
|
||||||
|
extern Unit_RTC RTC;
|
||||||
|
extern rtc_time_type rtc_time;
|
||||||
|
extern rtc_date_type rtc_date;
|
||||||
|
|
||||||
|
extern pvault::vault entries;
|
||||||
|
extern pvault::device_settings configuration;
|
||||||
|
extern uint8_t aes_key[pvault::key_size];
|
||||||
|
pvault::credential init_cred{};
|
||||||
|
|
||||||
|
extern int device_mode;
|
||||||
|
extern bool network_available;
|
||||||
|
extern bool rtc_available;
|
||||||
|
extern bool totp_available;
|
||||||
|
extern char totp_buffer[7];
|
||||||
|
|
||||||
|
extern bool mode0_preview;
|
||||||
|
extern int mode0_selection;
|
||||||
|
|
||||||
|
extern bool mode1_ispasswordbeingchanged;
|
||||||
|
uint8_t mode1_newpassword_tempkey[pvault::key_size];
|
||||||
|
extern String mode1_passwordinput;
|
||||||
|
|
||||||
|
extern int mode2_page;
|
||||||
|
|
||||||
|
extern int mode3_page;
|
||||||
|
extern String mode3_tempssid;
|
||||||
|
extern String mode3_tempwpwd;
|
||||||
|
extern String mode3_tempaddr;
|
||||||
|
extern String mode3_tempport;
|
||||||
|
extern String mode3_tempdpwd;
|
||||||
|
extern int mode3_tempbrightness;
|
||||||
|
|
||||||
|
|
||||||
|
extern int mode5_page;
|
||||||
|
const String mode5_interactive_hyperlinks[2] PROGMEM = {"https://github.com/adamecki/PWDer", "https://floriano.uk"};
|
||||||
|
|
||||||
|
extern String mode7_query;
|
||||||
|
extern bool mode7_show_results;
|
||||||
|
extern int mode7_index;
|
||||||
|
extern int mode7_matches;
|
||||||
|
extern int mode7_matchindex[100];
|
||||||
|
|
||||||
|
extern int auto_lock;
|
||||||
|
extern unsigned long last_action;
|
||||||
|
|
||||||
|
void press_with_preferred_keyboard(special_key k) {
|
||||||
|
if(ble_keyboard_ready()) {
|
||||||
|
ble_send_key(k);
|
||||||
|
} else {
|
||||||
|
usb_send_key(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void press_with_preferred_keyboard(char c) {
|
||||||
|
if(ble_keyboard_ready()) {
|
||||||
|
ble_send_key(c);
|
||||||
|
} else {
|
||||||
|
usb_send_key(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void check_keyboard_events() {
|
||||||
|
if(M5Cardputer.BtnA.wasPressed()) {
|
||||||
|
if(M5Cardputer.Keyboard.isKeyPressed(KEY_FN)) {
|
||||||
|
save_screenshot_bmp();
|
||||||
|
sfx::melody_c_major_scale();
|
||||||
|
} else {
|
||||||
|
// keyboard mode to be introduced
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (M5Cardputer.Keyboard.isChange()) {
|
||||||
|
last_action = millis();
|
||||||
|
|
||||||
|
if(auto_lock != 0) {
|
||||||
|
auto_lock = 0;
|
||||||
|
M5Cardputer.Display.setBrightness(BASIC_BRIGHTNESS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode0_preview && (device_mode == 0 || device_mode == 7)) {
|
||||||
|
mode0_preview = false;
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (M5Cardputer.Keyboard.isPressed()) {
|
||||||
|
if (device_mode != 1) { sfx::beep(); }
|
||||||
|
|
||||||
|
// for complex typing modes
|
||||||
|
Keyboard_Class::KeysState status = M5Cardputer.Keyboard.keysState();
|
||||||
|
|
||||||
|
// mode-specific actions
|
||||||
|
switch (device_mode) {
|
||||||
|
case 0:
|
||||||
|
if (M5Cardputer.Keyboard.isKeyPressed('m')) {
|
||||||
|
if(configuration.speaker_on_auto_lock & 1) {
|
||||||
|
configuration.speaker_on_auto_lock -= 1;
|
||||||
|
} else {
|
||||||
|
configuration.speaker_on_auto_lock += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pvault::update_config(VAULT_PATH, configuration);
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('q')) { // search (query)
|
||||||
|
device_mode = 7;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('h')) { // help
|
||||||
|
device_mode = 2;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('c')) { // about
|
||||||
|
device_mode = 5;
|
||||||
|
draw_ui();
|
||||||
|
} else if(M5Cardputer.Keyboard.isKeyPressed('b')) { // enable bluetooth keyboard
|
||||||
|
ble_keyboard_init();
|
||||||
|
// } else if (M5Cardputer.Keyboard.isKeyPressed('s')) { // synchronize (deprecated)
|
||||||
|
// device_mode = 4;
|
||||||
|
// draw_ui();
|
||||||
|
// net_password_import();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('o')) { // options
|
||||||
|
device_mode = 3;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('n')) {
|
||||||
|
retry_connection();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('l')) { // lock
|
||||||
|
device_mode = 1;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed(KEY_ENTER)) { // enter data using default mode
|
||||||
|
if (configuration.input_mode == 0 || configuration.input_mode == 2) {
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode0_selection].username).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode0_selection].username)[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.input_mode == 2) {
|
||||||
|
press_with_preferred_keyboard(special_key::TAB);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.input_mode == 1 || configuration.input_mode == 2) {
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode0_selection].password).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode0_selection].password)[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.input_mode == 2) {
|
||||||
|
press_with_preferred_keyboard(special_key::RETURN);
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('1')) { // enter username
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode0_selection].username).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode0_selection].username)[i]);
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('2')) { // enter password
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode0_selection].password).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode0_selection].password)[i]);
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('3')) { // enter all
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode0_selection].username).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode0_selection].username)[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
press_with_preferred_keyboard(special_key::TAB);
|
||||||
|
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode0_selection].password).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode0_selection].password)[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
press_with_preferred_keyboard(special_key::RETURN);
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('4')) { // Enter TOTP if available
|
||||||
|
if ((network_available || rtc_available) && totp_available) {
|
||||||
|
generate_totp(String(entries.credentials[mode0_selection].totp_secret));
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
press_with_preferred_keyboard(totp_buffer[i]);
|
||||||
|
}
|
||||||
|
totp_buffer[6] = '\0';
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('t')) { // press TAB on a computer
|
||||||
|
press_with_preferred_keyboard(special_key::TAB);
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('r')) { // press RETURN on a computer
|
||||||
|
press_with_preferred_keyboard(special_key::RETURN);
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('/') && mode0_selection < entries.credential_count) { // next password
|
||||||
|
mode0_selection++;
|
||||||
|
if (String(entries.credentials[mode0_selection].totp_secret) != "") {
|
||||||
|
totp_available = true;
|
||||||
|
} else {
|
||||||
|
totp_available = false;
|
||||||
|
}
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed(',') && mode0_selection > 1) { // previous password
|
||||||
|
mode0_selection--;
|
||||||
|
if (String(entries.credentials[mode0_selection].totp_secret) != "") {
|
||||||
|
totp_available = true;
|
||||||
|
} else {
|
||||||
|
totp_available = false;
|
||||||
|
}
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('v')) {
|
||||||
|
draw_ui();
|
||||||
|
mode0_preview = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
if (mode1_ispasswordbeingchanged &&
|
||||||
|
M5Cardputer.Keyboard.isKeyPressed(KEY_FN) && M5Cardputer.Keyboard.isKeyPressed('`')) {
|
||||||
|
device_mode = 3;
|
||||||
|
mode1_ispasswordbeingchanged = false;
|
||||||
|
mode1_passwordinput = "";
|
||||||
|
draw_ui();
|
||||||
|
push_icon(error, 4, 4, 1);
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
} else {
|
||||||
|
for (auto i : status.word) {
|
||||||
|
mode1_passwordinput += i;
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.del) {
|
||||||
|
mode1_passwordinput.remove(mode1_passwordinput.length() - 1);
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.enter) {
|
||||||
|
splash_screen();
|
||||||
|
splash_screen_create_progressbar();
|
||||||
|
delay(250);
|
||||||
|
splash_screen_update_progressbar_percentage(10);
|
||||||
|
|
||||||
|
// verify password
|
||||||
|
if(pvault::get_key(VAULT_PATH, mode1_passwordinput, mode1_newpassword_tempkey)) {
|
||||||
|
splash_screen_update_progressbar_percentage(33);
|
||||||
|
delay(250);
|
||||||
|
|
||||||
|
// password ok
|
||||||
|
if(mode1_ispasswordbeingchanged) {
|
||||||
|
// change password
|
||||||
|
// rewrite vault
|
||||||
|
pvault::init_vault(VAULT_PATH, mode3_tempdpwd, configuration, entries);
|
||||||
|
// obtain new key
|
||||||
|
memcpy(aes_key, mode1_newpassword_tempkey, pvault::key_size);
|
||||||
|
|
||||||
|
splash_screen_update_progressbar_percentage(100);
|
||||||
|
delay(250);
|
||||||
|
|
||||||
|
mode3_tempdpwd = "";
|
||||||
|
device_mode = 0;
|
||||||
|
draw_ui();
|
||||||
|
push_icon(ok, 4, 4, 1);
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
} else {
|
||||||
|
// unlock device
|
||||||
|
memcpy(aes_key, mode1_newpassword_tempkey, pvault::key_size);
|
||||||
|
pvault::load_vault(VAULT_PATH, aes_key, configuration, entries);
|
||||||
|
|
||||||
|
mode3_tempssid = String(entries.credentials[0].title);
|
||||||
|
mode3_tempwpwd = String(entries.credentials[0].username);
|
||||||
|
mode3_tempaddr = String(entries.credentials[0].password);
|
||||||
|
mode3_tempport = String(entries.credentials[0].totp_secret);
|
||||||
|
|
||||||
|
// if(SD.exists(IMPORT_FILE_PATH)) {
|
||||||
|
// device_mode = 6;
|
||||||
|
// } else {
|
||||||
|
device_mode = 0;
|
||||||
|
if(String(entries.credentials[mode0_selection].totp_secret) != "") { totp_available = true; }
|
||||||
|
// }
|
||||||
|
|
||||||
|
splash_screen_update_progressbar_percentage(100);
|
||||||
|
delay(250);
|
||||||
|
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
splash_screen_update_progressbar_percentage(33);
|
||||||
|
delay(250);
|
||||||
|
|
||||||
|
// wrong password
|
||||||
|
if(mode1_ispasswordbeingchanged) {
|
||||||
|
mode3_tempdpwd = "";
|
||||||
|
device_mode = 3;
|
||||||
|
draw_ui();
|
||||||
|
} else {
|
||||||
|
mode1_passwordinput = "";
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
push_icon(error, 4, 4, 1);
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
delay(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
mode1_passwordinput = "";
|
||||||
|
mode1_ispasswordbeingchanged = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
if (M5Cardputer.Keyboard.isKeyPressed('`')) {
|
||||||
|
if((mode2_page != 1) || (mode2_page == 1 && M5Cardputer.Keyboard.isKeyPressed(KEY_FN))) {
|
||||||
|
// challenge user to learn FN + ESC/arrows combinations
|
||||||
|
device_mode = 0;
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed(',') && mode2_page > 0) {
|
||||||
|
if((mode2_page != 1) || (mode2_page == 1 && M5Cardputer.Keyboard.isKeyPressed(KEY_FN))) {
|
||||||
|
mode2_page--;
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('/') && mode2_page < 10) {
|
||||||
|
if((mode2_page != 1) || (mode2_page == 1 && M5Cardputer.Keyboard.isKeyPressed(KEY_FN))) {
|
||||||
|
mode2_page++;
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('m')) {
|
||||||
|
if(configuration.speaker_on_auto_lock & 1) {
|
||||||
|
configuration.speaker_on_auto_lock -= 1;
|
||||||
|
} else {
|
||||||
|
configuration.speaker_on_auto_lock += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pvault::update_config(VAULT_PATH, configuration);
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('l')) {
|
||||||
|
device_mode = 1;
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
if (M5Cardputer.Keyboard.isKeyPressed('`') && (M5Cardputer.Keyboard.isKeyPressed(KEY_FN) || mode3_page == 0)) {
|
||||||
|
mode3_tempssid = String(entries.credentials[0].title);
|
||||||
|
mode3_tempwpwd = String(entries.credentials[0].username);
|
||||||
|
mode3_tempdpwd = "";
|
||||||
|
mode3_tempbrightness = configuration.brightness;
|
||||||
|
M5Cardputer.Display.setBrightness(configuration.brightness);
|
||||||
|
// mode3_tempaddr = String(entries.credentials[0].password);
|
||||||
|
// mode3_tempport = String(entries.credentials[0].totp_secret);
|
||||||
|
|
||||||
|
device_mode = 0;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed(KEY_FN) && M5Cardputer.Keyboard.isKeyPressed(',') &&
|
||||||
|
mode3_page > 0) {
|
||||||
|
mode3_page--;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed(KEY_FN) && M5Cardputer.Keyboard.isKeyPressed('/') &&
|
||||||
|
mode3_page < MODE3_PAGES_NUMBER - 1) {
|
||||||
|
mode3_page++;
|
||||||
|
draw_ui();
|
||||||
|
} else if(M5Cardputer.Keyboard.isKeyPressed(KEY_FN) && M5Cardputer.Keyboard.isKeyPressed(';') && mode3_page == 0) {
|
||||||
|
if(mode3_tempbrightness < 0xFF) {
|
||||||
|
mode3_tempbrightness += 0x10;
|
||||||
|
|
||||||
|
if((mode3_tempbrightness & 0xF) != 0xF) {
|
||||||
|
mode3_tempbrightness = mode3_tempbrightness | (0xF);
|
||||||
|
}
|
||||||
|
|
||||||
|
M5Cardputer.Display.setBrightness(mode3_tempbrightness);
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
} else if(M5Cardputer.Keyboard.isKeyPressed(KEY_FN) && M5Cardputer.Keyboard.isKeyPressed('.') && mode3_page == 0) {
|
||||||
|
if(mode3_tempbrightness > 0x0F) {
|
||||||
|
mode3_tempbrightness -= 0x10;
|
||||||
|
|
||||||
|
if(mode3_tempbrightness & 0xF) {
|
||||||
|
mode3_tempbrightness = (mode3_tempbrightness & ~0xF) | (0xF);
|
||||||
|
}
|
||||||
|
|
||||||
|
M5Cardputer.Display.setBrightness(mode3_tempbrightness);
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (auto i : status.word) {
|
||||||
|
switch (mode3_page) {
|
||||||
|
case 4:
|
||||||
|
mode3_tempdpwd += i;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
mode3_tempssid += i;
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
mode3_tempwpwd += i;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.del) {
|
||||||
|
switch (mode3_page) {
|
||||||
|
case 4:
|
||||||
|
mode3_tempdpwd.remove(mode3_tempdpwd.length() - 1);
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
mode3_tempssid.remove(mode3_tempssid.length() - 1);
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
mode3_tempwpwd.remove(mode3_tempwpwd.length() - 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.enter) {
|
||||||
|
switch (mode3_page) {
|
||||||
|
case 1:
|
||||||
|
switch (configuration.input_mode) {
|
||||||
|
case 0:
|
||||||
|
configuration.input_mode = 1;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
configuration.input_mode = 2;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
configuration.input_mode = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
draw_ui();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
switch(configuration.speaker_on_auto_lock) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
configuration.speaker_on_auto_lock += 2;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
configuration.speaker_on_auto_lock -= 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
if(configuration.color_scheme >= color_schemes_number - 1) {
|
||||||
|
configuration.color_scheme = 0;
|
||||||
|
} else {
|
||||||
|
configuration.color_scheme += 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
mode1_ispasswordbeingchanged = true;
|
||||||
|
device_mode = 1;
|
||||||
|
draw_ui();
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
if (rtc_available) {
|
||||||
|
if (!network_available) {
|
||||||
|
retry_connection();
|
||||||
|
}
|
||||||
|
if (network_available) {
|
||||||
|
timeClient.update();
|
||||||
|
struct tm new_time = to_regular_utc_timestamp(timeClient.getEpochTime());
|
||||||
|
|
||||||
|
rtc_time.Hours = new_time.tm_hour;
|
||||||
|
rtc_time.Minutes = new_time.tm_min;
|
||||||
|
rtc_time.Seconds = new_time.tm_sec;
|
||||||
|
RTC.setTime(&rtc_time);
|
||||||
|
|
||||||
|
rtc_date.Year = new_time.tm_year + 1900;
|
||||||
|
rtc_date.Month = new_time.tm_mon + 1;
|
||||||
|
rtc_date.Date = new_time.tm_mday;
|
||||||
|
RTC.setDate(&rtc_date);
|
||||||
|
}
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
case 7:
|
||||||
|
strncpy(init_cred.title, (char*)mode3_tempssid.c_str(), sizeof(init_cred.title));
|
||||||
|
strncpy(init_cred.username, (char*)mode3_tempwpwd.c_str(), sizeof(init_cred.username));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// things that need rewriting encrypted data
|
||||||
|
if(mode3_page == 6 || mode3_page == 7) {
|
||||||
|
entries.credentials[0] = init_cred;
|
||||||
|
pvault::update_vault(VAULT_PATH, aes_key, configuration, entries);
|
||||||
|
draw_ui();
|
||||||
|
push_icon(ok, 4, 4, 1);
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
}
|
||||||
|
// things that need rewriting header only
|
||||||
|
if(mode3_page == 0 || mode3_page == 1 || mode3_page == 2 || mode3_page == 3) {
|
||||||
|
configuration.brightness = mode3_tempbrightness;
|
||||||
|
pvault::update_config(VAULT_PATH, configuration);
|
||||||
|
draw_ui();
|
||||||
|
push_icon(ok, 4, 4, 1);
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
}
|
||||||
|
// things that just need graphical confirmation
|
||||||
|
if(mode3_page == 5) {
|
||||||
|
draw_ui();
|
||||||
|
push_icon(ok, 4, 4, 1);
|
||||||
|
canvas.pushSprite(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
if (M5Cardputer.Keyboard.isKeyPressed('m')) {
|
||||||
|
if(configuration.speaker_on_auto_lock & 1) {
|
||||||
|
configuration.speaker_on_auto_lock -= 1;
|
||||||
|
} else {
|
||||||
|
configuration.speaker_on_auto_lock += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pvault::update_config(VAULT_PATH, configuration);
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('`')) {
|
||||||
|
device_mode = 0;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed(KEY_ENTER)) {
|
||||||
|
for (int i = 0; i < mode5_interactive_hyperlinks[mode5_page].length(); i++) {
|
||||||
|
press_with_preferred_keyboard(mode5_interactive_hyperlinks[mode5_page][i]);
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('/') && mode5_page < 1) { // next page
|
||||||
|
mode5_page++;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed(',') && mode5_page > 0) { // previous page
|
||||||
|
mode5_page--;
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 7:
|
||||||
|
if (mode7_show_results) {
|
||||||
|
if (M5Cardputer.Keyboard.isKeyPressed('/') && mode7_index < mode7_matches - 1) { // next password
|
||||||
|
mode7_index++;
|
||||||
|
if (String(entries.credentials[mode7_matchindex[mode7_index]].totp_secret) != "") {
|
||||||
|
totp_available = true;
|
||||||
|
} else {
|
||||||
|
totp_available = false;
|
||||||
|
}
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed(',') && mode7_index > 0) { // previous password
|
||||||
|
mode7_index--;
|
||||||
|
if (String(entries.credentials[mode7_matchindex[mode7_index]].totp_secret) != "") {
|
||||||
|
totp_available = true;
|
||||||
|
} else {
|
||||||
|
totp_available = false;
|
||||||
|
}
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('`')) {
|
||||||
|
mode7_matches = 0;
|
||||||
|
mode7_index = 0;
|
||||||
|
mode7_show_results = false;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('l')) {
|
||||||
|
device_mode = 1;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('m')) {
|
||||||
|
if(configuration.speaker_on_auto_lock & 1) {
|
||||||
|
configuration.speaker_on_auto_lock -= 1;
|
||||||
|
} else {
|
||||||
|
configuration.speaker_on_auto_lock += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pvault::update_config(VAULT_PATH, configuration);
|
||||||
|
draw_ui();
|
||||||
|
} else if(M5Cardputer.Keyboard.isKeyPressed('b')) { // enable bluetooth keyboard
|
||||||
|
ble_keyboard_init();
|
||||||
|
} else if(M5Cardputer.Keyboard.isKeyPressed('n')) { // retry network connection
|
||||||
|
retry_connection();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('h')) { // help
|
||||||
|
device_mode = 2;
|
||||||
|
draw_ui();
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('t')) { // press TAB on a computer
|
||||||
|
press_with_preferred_keyboard(special_key::TAB);
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('r')) { // press RETURN on a computer
|
||||||
|
press_with_preferred_keyboard(special_key::RETURN);
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('v')) {
|
||||||
|
draw_ui();
|
||||||
|
mode0_preview = true;
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed(KEY_ENTER)) { // enter data using default mode
|
||||||
|
if (configuration.input_mode == 0 || configuration.input_mode == 2) {
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode7_matchindex[mode7_index]].username).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode7_matchindex[mode7_index]].username)[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.input_mode == 2) {
|
||||||
|
press_with_preferred_keyboard(special_key::TAB);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.input_mode == 1 || configuration.input_mode == 2) {
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode7_matchindex[mode7_index]].password).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode7_matchindex[mode7_index]].password)[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration.input_mode == 2) {
|
||||||
|
press_with_preferred_keyboard(special_key::RETURN);
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('1')) { // enter username
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode7_matchindex[mode7_index]].username).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode7_matchindex[mode7_index]].username)[i]);
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('2')) { // enter password
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode7_matchindex[mode7_index]].password).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode7_matchindex[mode7_index]].password)[i]);
|
||||||
|
}
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('3')) { // enter all
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode7_matchindex[mode7_index]].username).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode7_matchindex[mode7_index]].username)[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
press_with_preferred_keyboard(special_key::TAB);
|
||||||
|
|
||||||
|
for (int i = 0; i < String(entries.credentials[mode7_matchindex[mode7_index]].password).length(); i++) {
|
||||||
|
press_with_preferred_keyboard(String(entries.credentials[mode7_matchindex[mode7_index]].password)[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
press_with_preferred_keyboard(special_key::RETURN);
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('4')) { // Enter TOTP if available
|
||||||
|
if ((network_available || rtc_available) && totp_available) {
|
||||||
|
generate_totp(String(entries.credentials[mode7_matchindex[mode7_index]].totp_secret));
|
||||||
|
for (int i = 0; i < 6; i++) {
|
||||||
|
press_with_preferred_keyboard(totp_buffer[i]);
|
||||||
|
}
|
||||||
|
totp_buffer[6] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (M5Cardputer.Keyboard.isKeyPressed(KEY_FN) && M5Cardputer.Keyboard.isKeyPressed('`')) {
|
||||||
|
device_mode = 0;
|
||||||
|
if (String(entries.credentials[mode0_selection].totp_secret) != "") {
|
||||||
|
totp_available = true;
|
||||||
|
} else {
|
||||||
|
totp_available = false;
|
||||||
|
}
|
||||||
|
draw_ui();
|
||||||
|
} else {
|
||||||
|
for (auto i : status.word) {
|
||||||
|
mode7_query += i;
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.del) {
|
||||||
|
mode7_query.remove(mode7_query.length() - 1);
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.enter) {
|
||||||
|
if (mode7_query != "") {
|
||||||
|
String lowercase_query = mode7_query;
|
||||||
|
lowercase_query.toLowerCase();
|
||||||
|
|
||||||
|
for (int i = 1; i < entries.credential_count + 1; i++) {
|
||||||
|
String lowercase_title = String(entries.credentials[i].title);
|
||||||
|
lowercase_title.toLowerCase();
|
||||||
|
|
||||||
|
if (lowercase_title.indexOf(lowercase_query) != -1) {
|
||||||
|
mode7_matchindex[mode7_matches] = i;
|
||||||
|
mode7_matches++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mode7_show_results = true;
|
||||||
|
if (String(entries.credentials[mode7_matchindex[mode7_index]].totp_secret) != "") {
|
||||||
|
totp_available = true;
|
||||||
|
} else {
|
||||||
|
totp_available = false;
|
||||||
|
}
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
#include "globals.h"
|
||||||
|
|
||||||
|
#include "file_operations.h"
|
||||||
|
#include "gui.h"
|
||||||
|
#include "sfx.h"
|
||||||
|
#include "keyboard_events.h"
|
||||||
|
#include "network_operations.h"
|
||||||
|
#include "keyboard_bridge.h"
|
||||||
|
#include "time_operations.h"
|
||||||
|
#include "pwrotocol.h"
|
||||||
|
|
||||||
|
#define SCREEN_DIM_MS 15000
|
||||||
|
#define SCREEN_OFF_MS 20000
|
||||||
|
#define SCREEN_LCK_MS 30000
|
||||||
|
#define BASIC_BRIGHTNESS 0x80
|
||||||
|
#define DIMMED_BRIGHTNESS 0x40
|
||||||
|
|
||||||
|
M5Canvas canvas(&M5Cardputer.Display);
|
||||||
|
USBCDC USBSerialDevice;
|
||||||
|
WiFiUDP ntpUDP;
|
||||||
|
NTPClient timeClient(ntpUDP);
|
||||||
|
Unit_RTC RTC;
|
||||||
|
SPIClass sdcardSPI;
|
||||||
|
|
||||||
|
pvault::vault entries{};
|
||||||
|
pvault::device_settings configuration{};
|
||||||
|
uint8_t aes_key[pvault::key_size];
|
||||||
|
|
||||||
|
bool network_available = false;
|
||||||
|
bool network_initialized = false;
|
||||||
|
bool bluetooth_initialized = false;
|
||||||
|
bool totp_available = false;
|
||||||
|
char totp_buffer[7];
|
||||||
|
bool rtc_available = false;
|
||||||
|
rtc_time_type rtc_time;
|
||||||
|
rtc_date_type rtc_date;
|
||||||
|
uint8_t* motd_number = new uint8_t[1];
|
||||||
|
|
||||||
|
int8_t last_battery_percentage;
|
||||||
|
unsigned long last_ui_refresh;
|
||||||
|
|
||||||
|
int device_mode = 1;
|
||||||
|
// "Device mode" determines what does draw_ui() function draw and how does the device react to key presses in loop()
|
||||||
|
// 0 - main page
|
||||||
|
// 1 - device locked
|
||||||
|
// 2 - help page
|
||||||
|
// 3 - options page
|
||||||
|
// 4 - sync page (unused for now)
|
||||||
|
// 5 - credits page
|
||||||
|
// 6 - file import page (deprecated)
|
||||||
|
// 7 - search
|
||||||
|
|
||||||
|
unsigned long last_action = millis();
|
||||||
|
int auto_lock = 0;
|
||||||
|
// 0 - normal work
|
||||||
|
// 1 - screen dimmed
|
||||||
|
// 2 - screen off
|
||||||
|
|
||||||
|
int mode0_selection = 1;
|
||||||
|
bool mode0_preview = false;
|
||||||
|
|
||||||
|
String mode1_passwordinput = "";
|
||||||
|
bool mode1_ispasswordbeingchanged = false;
|
||||||
|
|
||||||
|
int mode2_page = 0;
|
||||||
|
|
||||||
|
int mode3_page = 0;
|
||||||
|
// 0 - default password input mode
|
||||||
|
// 1 - ssid
|
||||||
|
// 2 - wifi pwd
|
||||||
|
// 3 - ip
|
||||||
|
// 4 - port
|
||||||
|
// 5 - devpwd
|
||||||
|
// 6 - wifi timeout
|
||||||
|
// * - ntp server
|
||||||
|
// 7 - ntp time sync
|
||||||
|
// * - manual rtc clock configuration
|
||||||
|
// 8 - export vault
|
||||||
|
String mode3_tempssid = "";
|
||||||
|
String mode3_tempwpwd = "";
|
||||||
|
String mode3_tempaddr = "";
|
||||||
|
String mode3_tempport = "";
|
||||||
|
String mode3_tempdpwd = "";
|
||||||
|
uint8_t mode3_tempbrightness = BASIC_BRIGHTNESS;
|
||||||
|
|
||||||
|
int mode4_page = 0;
|
||||||
|
|
||||||
|
int mode5_page = 0;
|
||||||
|
|
||||||
|
String mode7_query = "";
|
||||||
|
int mode7_matchindex[100];
|
||||||
|
int mode7_index = 0;
|
||||||
|
int mode7_matches = 0;
|
||||||
|
bool mode7_show_results = false;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
auto cfg = M5.config();
|
||||||
|
M5Cardputer.begin(cfg, true);
|
||||||
|
|
||||||
|
uint8_t brightness = 0x00;
|
||||||
|
M5Cardputer.Display.setBrightness(brightness);
|
||||||
|
|
||||||
|
M5Cardputer.Display.setRotation(1);
|
||||||
|
|
||||||
|
canvas.createSprite(M5Cardputer.Display.width(), M5Cardputer.Display.height());
|
||||||
|
canvas.setFont(&fonts::Font2);
|
||||||
|
canvas.setTextColor(WHITE);
|
||||||
|
canvas.setTextSize(1);
|
||||||
|
|
||||||
|
esp_fill_random(motd_number, sizeof(uint8_t));
|
||||||
|
motd_number[0] /= 8;
|
||||||
|
|
||||||
|
splash_screen();
|
||||||
|
|
||||||
|
while(brightness < BASIC_BRIGHTNESS) {
|
||||||
|
brightness += 1;
|
||||||
|
M5Cardputer.Display.setBrightness(brightness);
|
||||||
|
delay(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
splash_screen_create_progressbar();
|
||||||
|
|
||||||
|
// start sdcard
|
||||||
|
sdcardSPI.begin(SD_CLK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
|
||||||
|
if (!SD.begin(SD_CS_PIN, sdcardSPI)) {
|
||||||
|
no_sdcard_crash_screen();
|
||||||
|
|
||||||
|
// halt system if no sdcard found
|
||||||
|
while (1);
|
||||||
|
}
|
||||||
|
|
||||||
|
splash_screen_update_progressbar_percentage(25);
|
||||||
|
|
||||||
|
// read config or write a sample
|
||||||
|
if(!SD.exists(VAULT_PATH)) {
|
||||||
|
init_new_vault();
|
||||||
|
} else {
|
||||||
|
pvault::read_config(VAULT_PATH, configuration);
|
||||||
|
}
|
||||||
|
if(configuration.brightness < 0x0F) {
|
||||||
|
configuration.brightness = 0x0F;
|
||||||
|
}
|
||||||
|
M5Cardputer.Display.setBrightness(configuration.brightness);
|
||||||
|
mode3_tempbrightness = configuration.brightness;
|
||||||
|
|
||||||
|
if(SD.exists(EXPORT_FILE_PATH)) {
|
||||||
|
SD.remove(EXPORT_FILE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update splash screen with new theme when it's loaded
|
||||||
|
splash_screen();
|
||||||
|
|
||||||
|
M5Cardputer.update();
|
||||||
|
if(M5Cardputer.BtnA.isPressed() && M5Cardputer.Keyboard.isKeyPressed(KEY_FN)) {
|
||||||
|
save_screenshot_bmp();
|
||||||
|
sfx::melody_c_major_scale();
|
||||||
|
}
|
||||||
|
|
||||||
|
splash_screen_create_progressbar();
|
||||||
|
splash_screen_update_progressbar_percentage(50);
|
||||||
|
|
||||||
|
// mode3_tempssid = entries.credentials[0].title;
|
||||||
|
// mode3_tempwpwd = entries.credentials[0].username;
|
||||||
|
// mode3_tempaddr = entries.credentials[0].password;
|
||||||
|
// mode3_tempport = entries.credentials[0].totp_secret;
|
||||||
|
|
||||||
|
// start keyboard and serial port
|
||||||
|
USB.begin();
|
||||||
|
USBSerialDevice.begin(115200);
|
||||||
|
usb_keyboard_init(); // USB by default, ble attempts to connect only when B is pressed
|
||||||
|
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
splash_screen_update_progressbar_percentage(75);
|
||||||
|
|
||||||
|
// try getting time
|
||||||
|
setenv("TZ", "UTCO", 1);
|
||||||
|
tzset();
|
||||||
|
|
||||||
|
rtc_available = start_rtc();
|
||||||
|
|
||||||
|
last_battery_percentage = M5.Power.getBatteryLevel();
|
||||||
|
last_ui_refresh = millis();
|
||||||
|
|
||||||
|
splash_screen_update_progressbar_percentage(100);
|
||||||
|
delay(250);
|
||||||
|
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
M5Cardputer.update();
|
||||||
|
|
||||||
|
int8_t current_battery_percentage = M5.Power.getBatteryLevel();
|
||||||
|
|
||||||
|
if ((current_battery_percentage != last_battery_percentage) && millis() - last_ui_refresh > UI_UPDATE_MILISECONDS && auto_lock < 2) {
|
||||||
|
last_battery_percentage = current_battery_percentage;
|
||||||
|
ble_keyboard_update_battery_level();
|
||||||
|
last_ui_refresh = millis();
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED && network_available == false) {
|
||||||
|
network_available = true;
|
||||||
|
} else if (WiFi.status() != WL_CONNECTED && network_available == true) {
|
||||||
|
network_available = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for serial commands
|
||||||
|
pwrotocol_listen_and_respond();
|
||||||
|
|
||||||
|
check_keyboard_events();
|
||||||
|
|
||||||
|
if(configuration.speaker_on_auto_lock == 2 || configuration.speaker_on_auto_lock == 3) {
|
||||||
|
if((millis() - last_action >= SCREEN_DIM_MS) && auto_lock == 0) {
|
||||||
|
auto_lock = 1;
|
||||||
|
if(M5Cardputer.Display.getBrightness() > DIMMED_BRIGHTNESS) {
|
||||||
|
M5Cardputer.Display.setBrightness(DIMMED_BRIGHTNESS);
|
||||||
|
}
|
||||||
|
} else if((millis() - last_action >= SCREEN_OFF_MS) && auto_lock == 1) {
|
||||||
|
auto_lock = 2;
|
||||||
|
M5Cardputer.Display.setBrightness(0x00);
|
||||||
|
} else if((millis() - last_action >= SCREEN_LCK_MS) && auto_lock == 2) {
|
||||||
|
auto_lock = 3;
|
||||||
|
device_mode = 1;
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
#include "globals.h"
|
||||||
|
#include "keyboard_bridge.h"
|
||||||
|
#include "gui.h"
|
||||||
|
|
||||||
|
extern NTPClient timeClient;
|
||||||
|
extern bool network_available;
|
||||||
|
extern int device_mode;
|
||||||
|
extern int mode4_page;
|
||||||
|
extern bool network_initialized;
|
||||||
|
extern bool bluetooth_initialized;
|
||||||
|
|
||||||
|
extern pvault::vault entries;
|
||||||
|
const int wifi_timeout = 5;
|
||||||
|
|
||||||
|
void retry_connection(bool reconnect = true) {
|
||||||
|
if(bluetooth_initialized) {
|
||||||
|
connection_init_error();
|
||||||
|
delay(3000);
|
||||||
|
draw_ui();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reconnect) {
|
||||||
|
timeClient.end();
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
delay(100);
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
delay(100);
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
delay(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wifi_timeout != 0) {
|
||||||
|
WiFi.begin(entries.credentials[0].title, entries.credentials[0].username);
|
||||||
|
|
||||||
|
int timeout_500ms = 0;
|
||||||
|
|
||||||
|
while (WiFi.status() != WL_CONNECTED) {
|
||||||
|
delay(500);
|
||||||
|
timeout_500ms++;
|
||||||
|
if (timeout_500ms >= (2 * wifi_timeout)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
network_available = false;
|
||||||
|
|
||||||
|
if (timeout_500ms < (2 * wifi_timeout)) {
|
||||||
|
network_available = true;
|
||||||
|
timeClient.begin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
network_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
void read_response(WiFiClient* client, String& lines) {
|
||||||
|
unsigned long timeout = millis();
|
||||||
|
while (client->available() == 0) {
|
||||||
|
if (millis() - timeout > 5000) {
|
||||||
|
client->stop();
|
||||||
|
mode4_page = 5;
|
||||||
|
draw_ui();
|
||||||
|
delay(3000);
|
||||||
|
mode4_page = 0;
|
||||||
|
device_mode = 0;
|
||||||
|
draw_ui();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (client->available()) {
|
||||||
|
lines += client->readStringUntil('\r');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void net_password_import() {
|
||||||
|
if (network_available) {
|
||||||
|
const char* host = entries.credentials[0].password;
|
||||||
|
const int port = (int)entries.credentials[0].totp_secret;
|
||||||
|
|
||||||
|
String import_string = "";
|
||||||
|
|
||||||
|
mode4_page = 1;
|
||||||
|
draw_ui();
|
||||||
|
|
||||||
|
WiFiClient client;
|
||||||
|
String footer = String(" HTTP/1.1\r\n") + "Host: " + String(host) + "\r\n" + "Connection: close\r\n\r\n";
|
||||||
|
String readRequest = "GET /" + footer;
|
||||||
|
|
||||||
|
if (!client.connect(host, port)) {
|
||||||
|
mode4_page = 5;
|
||||||
|
draw_ui();
|
||||||
|
delay(3000);
|
||||||
|
mode4_page = 0;
|
||||||
|
device_mode = 0;
|
||||||
|
draw_ui();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mode4_page = 2;
|
||||||
|
draw_ui();
|
||||||
|
|
||||||
|
client.print(readRequest);
|
||||||
|
read_response(&client, import_string);
|
||||||
|
|
||||||
|
if (SD.exists(IMPORT_FILE_PATH)) {
|
||||||
|
SD.remove(IMPORT_FILE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
File import_file = SD.open(IMPORT_FILE_PATH, FILE_WRITE);
|
||||||
|
|
||||||
|
int lines_read = 0;
|
||||||
|
int start = 0;
|
||||||
|
while (true) {
|
||||||
|
int end = import_string.indexOf('\n', start);
|
||||||
|
if (end == -1) {
|
||||||
|
String line = import_string.substring(start);
|
||||||
|
import_file.print(line + String('\n'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
String line = import_string.substring(start, end);
|
||||||
|
start = end + 1;
|
||||||
|
if (lines_read > 4) {
|
||||||
|
import_file.print(line + String('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines_read++;
|
||||||
|
}
|
||||||
|
|
||||||
|
import_file.close();
|
||||||
|
|
||||||
|
if (lines_read > 7) {
|
||||||
|
mode4_page = 3;
|
||||||
|
draw_ui();
|
||||||
|
delay(3000);
|
||||||
|
|
||||||
|
mode4_page = 0;
|
||||||
|
device_mode = 6;
|
||||||
|
draw_ui();
|
||||||
|
} else {
|
||||||
|
if (SD.exists(IMPORT_FILE_PATH)) {
|
||||||
|
SD.remove(IMPORT_FILE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
mode4_page = 6;
|
||||||
|
draw_ui();
|
||||||
|
delay(3000);
|
||||||
|
|
||||||
|
mode4_page = 0;
|
||||||
|
device_mode = 0;
|
||||||
|
draw_ui();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mode4_page = 4;
|
||||||
|
draw_ui();
|
||||||
|
delay(3000);
|
||||||
|
mode4_page = 0;
|
||||||
|
device_mode = 0;
|
||||||
|
draw_ui();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
#include "globals.h"
|
||||||
|
#include "pwrotocol.h"
|
||||||
|
#include "gui.h"
|
||||||
|
|
||||||
|
extern pvault::vault entries;
|
||||||
|
extern pvault::device_settings configuration;
|
||||||
|
extern uint8_t aes_key[pvault::key_size];
|
||||||
|
extern USBCDC USBSerialDevice;
|
||||||
|
extern int device_mode;
|
||||||
|
extern void draw_ui();
|
||||||
|
|
||||||
|
void pwrotocol_listen_and_respond() {
|
||||||
|
if(USBSerialDevice.available()) {
|
||||||
|
uint8_t cmd = USBSerialDevice.read();
|
||||||
|
uint8_t salt[pvault::salt_size];
|
||||||
|
|
||||||
|
switch(cmd) {
|
||||||
|
case PWROTOCOL_PING:
|
||||||
|
USBSerialDevice.write("OK", 2);
|
||||||
|
break;
|
||||||
|
case PWROTOCOL_GET_SALT:
|
||||||
|
if(!pvault::get_salt(VAULT_PATH, salt)) {
|
||||||
|
USBSerialDevice.write("FAIL", sizeof("FAIL"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
USBSerialDevice.write(salt, pvault::salt_size);
|
||||||
|
break;
|
||||||
|
case PWROTOCOL_GET_ITERATIONS:
|
||||||
|
USBSerialDevice.write(reinterpret_cast<const uint8_t*>(&pvault::iterations), sizeof(pvault::iterations));
|
||||||
|
break;
|
||||||
|
case PWROTOCOL_REQUEST_CONF:
|
||||||
|
USBSerialDevice.write(reinterpret_cast<const uint8_t*>(&entries.credentials[0]), sizeof(pvault::credential));
|
||||||
|
break;
|
||||||
|
case PWROTOCOL_FILE_IMPORT:
|
||||||
|
if(device_mode != 0) { break; }
|
||||||
|
|
||||||
|
if(SD.exists(IMPORT_FILE_PATH)) { SD.remove(IMPORT_FILE_PATH); }
|
||||||
|
File import_file = SD.open(IMPORT_FILE_PATH, FILE_WRITE);
|
||||||
|
|
||||||
|
USBSerialDevice.write("OK", 2);
|
||||||
|
USBSerialDevice.flush();
|
||||||
|
|
||||||
|
// === MAGIC ===
|
||||||
|
// wait for magic
|
||||||
|
unsigned long wait = millis();
|
||||||
|
while(USBSerialDevice.available() < 8) {
|
||||||
|
delay(1);
|
||||||
|
if(millis() - wait > PWROTOCOL_TIMEOUT_MS) {
|
||||||
|
// too long
|
||||||
|
import_file.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive magic
|
||||||
|
char magic[8];
|
||||||
|
if(USBSerialDevice.readBytes((char*)magic, 8) != 8) {
|
||||||
|
import_file.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(memcmp(magic, "PWIMPORT", 8) != 0) { return; }
|
||||||
|
|
||||||
|
import_file.write(reinterpret_cast<const uint8_t*>(magic), 8);
|
||||||
|
USBSerialDevice.write("OK", 2);
|
||||||
|
USBSerialDevice.flush();
|
||||||
|
|
||||||
|
// === IV ===
|
||||||
|
// wait for nonce
|
||||||
|
wait = millis();
|
||||||
|
while(USBSerialDevice.available() < pvault::nonce_size) {
|
||||||
|
delay(1);
|
||||||
|
if(millis() - wait > PWROTOCOL_TIMEOUT_MS) {
|
||||||
|
// too long
|
||||||
|
import_file.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive nonce
|
||||||
|
uint8_t nonce[pvault::nonce_size];
|
||||||
|
if(USBSerialDevice.readBytes((char*)nonce, pvault::nonce_size) != pvault::nonce_size) { return; }
|
||||||
|
|
||||||
|
import_file.write(nonce, pvault::nonce_size);
|
||||||
|
USBSerialDevice.write("OK", 2);
|
||||||
|
USBSerialDevice.flush();
|
||||||
|
|
||||||
|
// === LEN ===
|
||||||
|
// wait for ciphertext length
|
||||||
|
wait = millis();
|
||||||
|
while(USBSerialDevice.available() < sizeof(uint32_t)) {
|
||||||
|
delay(1);
|
||||||
|
if(millis() - wait > PWROTOCOL_TIMEOUT_MS) {
|
||||||
|
// too long
|
||||||
|
import_file.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive ciphertext length
|
||||||
|
uint32_t len;
|
||||||
|
if(USBSerialDevice.readBytes(reinterpret_cast<char*>(&len), sizeof(len)) != sizeof(len)) { return; }
|
||||||
|
if(len != sizeof(pvault::vault)) { return; }
|
||||||
|
|
||||||
|
import_file.write(reinterpret_cast<const uint8_t*>(&len), sizeof(uint32_t));
|
||||||
|
USBSerialDevice.write("OK", 2);
|
||||||
|
USBSerialDevice.flush();
|
||||||
|
|
||||||
|
// === CIPHERTEXT ===
|
||||||
|
// receive chunks of ciphertext
|
||||||
|
|
||||||
|
uint8_t chunk_bytes[PWROTOCOL_CHUNK_SIZE];
|
||||||
|
uint32_t bytes_received = 0;
|
||||||
|
wait = millis();
|
||||||
|
|
||||||
|
uint32_t chunk_target;
|
||||||
|
uint32_t chunk_received;
|
||||||
|
|
||||||
|
while(bytes_received < len) {
|
||||||
|
if(len - bytes_received > PWROTOCOL_CHUNK_SIZE) {
|
||||||
|
chunk_target = PWROTOCOL_CHUNK_SIZE;
|
||||||
|
} else {
|
||||||
|
chunk_target = len - bytes_received;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk_received = 0;
|
||||||
|
|
||||||
|
while(chunk_received < chunk_target) {
|
||||||
|
int avail = USBSerialDevice.available();
|
||||||
|
|
||||||
|
if(avail > 0) {
|
||||||
|
uint32_t to_read = chunk_target - chunk_received;
|
||||||
|
if((uint32_t)avail < to_read) { to_read = avail; }
|
||||||
|
|
||||||
|
int r = USBSerialDevice.readBytes(reinterpret_cast<char*>(chunk_bytes + chunk_received), to_read);
|
||||||
|
|
||||||
|
if(r <= 0) {
|
||||||
|
import_file.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk_received += r;
|
||||||
|
wait = millis();
|
||||||
|
} else {
|
||||||
|
delay(1);
|
||||||
|
if(millis() - wait > PWROTOCOL_TIMEOUT_MS) {
|
||||||
|
return;
|
||||||
|
import_file.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t written = import_file.write(chunk_bytes, chunk_target);
|
||||||
|
if(written != chunk_target) {
|
||||||
|
import_file.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
USBSerialDevice.write("OK", 2);
|
||||||
|
USBSerialDevice.flush();
|
||||||
|
|
||||||
|
bytes_received += chunk_target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === TAG ===
|
||||||
|
// wait for tag
|
||||||
|
wait = millis();
|
||||||
|
while(USBSerialDevice.available() < pvault::tag_size) {
|
||||||
|
delay(1);
|
||||||
|
if(millis() - wait > PWROTOCOL_TIMEOUT_MS) {
|
||||||
|
// too long
|
||||||
|
import_file.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive tag
|
||||||
|
uint8_t tag[pvault::tag_size];
|
||||||
|
if(USBSerialDevice.readBytes((char*)tag, pvault::tag_size) != pvault::tag_size) {
|
||||||
|
import_file.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
import_file.write(tag, pvault::tag_size);
|
||||||
|
USBSerialDevice.write("OK", 2);
|
||||||
|
USBSerialDevice.flush();
|
||||||
|
|
||||||
|
import_file.close();
|
||||||
|
|
||||||
|
// === IMPORT ===
|
||||||
|
// ask
|
||||||
|
password_import_screen();
|
||||||
|
while(1) {
|
||||||
|
M5Cardputer.update();
|
||||||
|
if(M5Cardputer.Keyboard.isKeyPressed('n')) {
|
||||||
|
draw_ui();
|
||||||
|
return;
|
||||||
|
} else if (M5Cardputer.Keyboard.isKeyPressed('y')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get salt from vault file
|
||||||
|
uint8_t salt[pvault::salt_size];
|
||||||
|
if(!pvault::get_salt(VAULT_PATH, salt)) {
|
||||||
|
draw_ui();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// import provided data
|
||||||
|
pvault::replace_vault(VAULT_PATH, configuration, aes_key, salt, IMPORT_FILE_PATH);
|
||||||
|
pvault::load_vault(VAULT_PATH, aes_key, configuration, entries);
|
||||||
|
draw_ui();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#include "sfx.h"
|
||||||
|
extern pvault::device_settings configuration;
|
||||||
|
|
||||||
|
namespace sfx {
|
||||||
|
void beep() {
|
||||||
|
if(configuration.speaker_on_auto_lock & 1) {
|
||||||
|
M5Cardputer.Speaker.tone(8000, 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void melody_c_major_scale() {
|
||||||
|
if(configuration.speaker_on_auto_lock & 1) {
|
||||||
|
M5Cardputer.Speaker.tone(261.63, 100);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
M5Cardputer.Speaker.tone(293.66, 100);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
M5Cardputer.Speaker.tone(329.63, 100);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
M5Cardputer.Speaker.tone(349.23, 100);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
M5Cardputer.Speaker.tone(392, 100);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
M5Cardputer.Speaker.tone(440, 100);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
M5Cardputer.Speaker.tone(493.88, 100);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
M5Cardputer.Speaker.tone(523.25, 100);
|
||||||
|
delay(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#include "globals.h"
|
||||||
|
|
||||||
|
extern bool totp_available;
|
||||||
|
extern bool network_available;
|
||||||
|
extern bool rtc_available;
|
||||||
|
extern Unit_RTC RTC;
|
||||||
|
extern rtc_time_type rtc_time;
|
||||||
|
extern rtc_date_type rtc_date;
|
||||||
|
extern NTPClient timeClient;
|
||||||
|
extern char totp_buffer[7];
|
||||||
|
|
||||||
|
unsigned long to_unix_utc_timestamp(int year, int month, int day, int hour, int minute, int second) {
|
||||||
|
struct tm t;
|
||||||
|
|
||||||
|
t.tm_year = year - 1900;
|
||||||
|
t.tm_mon = month - 1;
|
||||||
|
t.tm_mday = day;
|
||||||
|
t.tm_hour = hour;
|
||||||
|
t.tm_min = minute;
|
||||||
|
t.tm_sec = second;
|
||||||
|
t.tm_isdst = 0;
|
||||||
|
|
||||||
|
time_t result = mktime(&t);
|
||||||
|
if (result == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return (long)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct tm to_regular_utc_timestamp(unsigned long epoch) {
|
||||||
|
time_t timestamp = static_cast<time_t>(epoch);
|
||||||
|
struct tm timeinfo;
|
||||||
|
gmtime_r(×tamp, &timeinfo);
|
||||||
|
return timeinfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void generate_totp(String secret) {
|
||||||
|
if (totp_available && (network_available || rtc_available)) {
|
||||||
|
if (rtc_available) { // Use RTC as a primary time provider
|
||||||
|
RTC.getDate(&rtc_date);
|
||||||
|
RTC.getTime(&rtc_time);
|
||||||
|
} else {
|
||||||
|
timeClient.update();
|
||||||
|
}
|
||||||
|
const int decoded_len = secret.length();
|
||||||
|
uint8_t decoded[decoded_len];
|
||||||
|
int keyLen = base32decode(secret.c_str(), decoded, sizeof(decoded));
|
||||||
|
TOTP totp = TOTP(decoded, keyLen);
|
||||||
|
if (rtc_available) {
|
||||||
|
strncpy(totp_buffer,
|
||||||
|
totp.getCode(to_unix_utc_timestamp(rtc_date.Year, rtc_date.Month, rtc_date.Date, rtc_time.Hours,
|
||||||
|
rtc_time.Minutes, rtc_time.Seconds)),
|
||||||
|
sizeof(totp_buffer));
|
||||||
|
} else {
|
||||||
|
strncpy(totp_buffer, totp.getCode(timeClient.getEpochTime()), sizeof(totp_buffer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool start_rtc() {
|
||||||
|
RTC.begin();
|
||||||
|
delay(10);
|
||||||
|
RTC.getDate(&rtc_date);
|
||||||
|
if (rtc_date.Year == 2000) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||