Skip to content

Commit

Permalink
Allow PIN to be changed (#6)
Browse files Browse the repository at this point in the history
* add pin setting and resetting
* encapsulate LED (prep for other board support)
  • Loading branch information
virgesmith authored Apr 27, 2024
1 parent 293f9c4 commit f9bd708
Show file tree
Hide file tree
Showing 27 changed files with 531 additions and 335 deletions.
29 changes: 26 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ add_executable(${PICO_IMAGE}
src/sha256.cpp
src/ecdsa.cpp
src/aes.cpp
src/device.cpp
src/flash.cpp
src/board.cpp
src/error.cpp
src/usb_descriptors.c
)
Expand All @@ -28,5 +29,27 @@ target_include_directories(${PICO_IMAGE} PUBLIC ${CMAKE_CURRENT_LIST_DIR}/src ${
# include_directories(${CMAKE_SOURCE_DIR}/mbedtls/include)
target_compile_options(${PICO_IMAGE} PRIVATE -Wno-psabi -Werror -Wall -DMBEDTLS_ALLOW_PRIVATE_ACCESS)
# warning disabled due to this: pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040/rp2040_usb.h:139
target_link_libraries(${PICO_IMAGE} pico_stdlib pico_unique_id tinyusb_device tinyusb_board mbedcrypto)
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20")
target_link_libraries(${PICO_IMAGE}
pico_stdlib
pico_unique_id
hardware_flash
hardware_sync
tinyusb_device
tinyusb_board
mbedcrypto
)

message("Target is ${PICO_RESET_PIN}")
add_executable(${PICO_RESET_PIN}
src/reset_pin.cpp
src/flash.cpp
src/board.cpp
src/error.cpp
)
target_compile_options(${PICO_RESET_PIN} PRIVATE -Wno-psabi -Werror -Wall -DMBEDTLS_ALLOW_PRIVATE_ACCESS)
target_link_libraries(${PICO_RESET_PIN}
pico_stdlib
hardware_flash
hardware_sync
)
pico_add_extra_outputs(${PICO_RESET_PIN})
124 changes: 82 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ I'm not a security expert and the device/software is almost certainly not harden
- the private key is only initialised once a correct pin has been entered, and is a sha256 hash of the (salted) unique device id of the pico. So no two devices should have the same key.
- the private key never leaves the device and is stored only in volatile memory.

NB This app can be installed on a Pico W and will work, with the exception of the onboard LED - to make this work requires the wifi drivers to be compiled into the project.

## Update v1.2.0

The device pin is now configurable. See [PIN protection](#pin-protection) and the [change pin](#change-pin) example.

## Update v1.1.0

The device now uses USB CDC rather than serial to communicate with the host which allows much faster bitrates and avoids the need to encode binary data. Performance is improved, but varies considerably by task (results are for a 1000k input):
The device now uses USB CDC rather than serial to communicate with the host which allows much faster bitrates and avoids the need to encode binary data. Performance is improved, but varies considerably by task (results are for a 1000kB input):

| task | CDC<br>time(s) | CDC<br>bitrate(kbps) | serial<br>time(s) | serial<br>bitrate(kbps)| Speedup(%) |
|:--------|---------------:|---------------------:|------------------:|-----------------------:|-----------:|
Expand All @@ -26,8 +32,6 @@ The device now uses USB CDC rather than serial to communicate with the host whic
| encrypt | 23.9 | 334.2 | 43.5 | 183.8 | 81.9 |
| decrypt | 23.8 | 336.0 | 43.1 | 185.7 | 81.0 |



## Dependencies/prerequisites

`pico-crypto-key` comes as a python (dev) package that provides:
Expand Down Expand Up @@ -70,31 +74,32 @@ You will then need to:
ln -s ../mbedtls-3.6.0 mbedtls
```

Not sure why, but I couldn't get it to work with the symlink inside pico-sdk like tinyusb.

You should now have a structure something like this:

```txt
.
├── mbedtls-3.6.0
├── pico-crypto-key
│ ├── examples
│ ├── mbedtls -> ../mbedtls-3.6.0
│ ├── pico_crypto_key
│ │ ├── build.py
│ │ ├── device.py
│ │ └── __init__.py
│ ├── pico-sdk -> ../pico-sdk-1.5.1
│ ├── pyproject.toml
│ ├── README.md
│ ├── setup.cfg
│ ├── src
│ └── test
├── pico-sdk-1.5.1
│ └── lib
│ └── tinyusb -> ../../tinyusb-0.16.0
└── tinyusb-0.16.0
├──mbedtls-3.6.0
├──pico-crypto-key
│ ├──examples
│ ├──mbedtls -> ../mbedtls-3.6.0
│ ├──pico_crypto_key
│ │ ├──build.py
│ │ ├──device.py
│ │ └──__init__.py
│ ├──pico-sdk -> ../pico-sdk-1.5.1
│ ├──pyproject.toml
│ ├──README.md
│ ├──src
│ └──test
├──pico-sdk-1.5.1
│ └──lib
│ └──tinyusb -> ../../tinyusb-0.16.0
└──tinyusb-0.16.0
```

### Configure
## Configure

If using a fresh download of `mbedtls` - run the configuration script to customise the build for the pico, e.g.:

Expand All @@ -106,25 +111,37 @@ More info [here](https://tls.mbed.org/discussions/generic/mbedtls-build-for-arm)

## Build

These steps use the `picobuild` script. Optionally check your configuration looks correct then build:
These steps use the `picobuild` script. (See `picobuild --help`.) Optionally check your configuration looks correct then build:

```sh
picobuild check
picobuild build
```

Ensure your device is connected and mounted ready to accept a new image (press BOOTSEL when connecting), then:
Ensure your device is connected and mounted ready to accept a new image (press `BOOTSEL` when connecting), then:

```sh
picobuild install /path/to/RPI-RP2
picobuild test
```

(The device PIN is currently defined in [pyproject.toml](./pyproject.toml))
## PIN protection

The device is protected with a PIN, the salted hash of which is read from flash memory. Before first use (or a forgotten PIN), a hash must be written to flash (press `BOOTSEL` when connecting):

```sh
picobuild reset-pin /path/to/RPI-RP2
```

If the device LED is flashing after this, the reset failed - the flash memory may be worn. Otherwise now reinstall the crypto key image as above. The pin will then be "pico", and it can be changed - see the [example](#change-pin).

The python driver will first check for an env var `PICO_CRYPTO_KEY_PIN` and fall back to a prompt if this is not present.

(NB for the tests to run, the env var *must* be set)

## Using the device

The device is pin protected (the word 'pico'), and (for now) it can't be changed without editing the code.
The device is pin protected (default is the word 'pico')

The `CryptoKey` class provides the python interface and is context-managed to help ensure the device gets properly opened and closed. The correct pin must be provided to activate it.

Expand All @@ -134,13 +151,29 @@ The `CryptoKey` class provides the python interface and is context-managed to he
- `verify` verify the given hash matches the signature and public key
- `encrypt` encrypts using AES256
- `decrypt` decrypts using AES256

Both the tests and examples read the pin from the `[pico.run]` section in [pyproject.toml](./pyproject.toml). Modify the settings as necessary.
- `set_pin` set a new PIN

See the examples for more details.


### Troubleshooting
## Errors

The device LED is normally off when the device is idle, and on when it's doing something. If there are low-level errors with any of the crypto algorithms then the device may enter an error state where the LED will flash. The error codes can be interpreted like so:

Long flashes | Short flashes | Algorithm | mbedtls error code
------------:|--------------:|-----------|-------------------
1 | 0 | ECDSA | Unknown error
1 | 1 | ECDSA | `MBEDTLS_ERR_ECP_BAD_INPUT_DATA`
1 | 2 | ECDSA | `MBEDTLS_ERR_ECP_BUFFER_TOO_SMALL`
1 | 3 | ECDSA | `MBEDTLS_ERR_ECP_FEATURE_UNAVAILABLE`
1 | 4 | ECDSA | `MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED`
1 | 5 | ECDSA | `MBEDTLS_ERR_MPI_ALLOC_FAILED`
2 | 0 | AES | Unknown error
2 | 1 | AES | `MBEDTLS_ERR_AES_INVALID_KEY_LENGTH`
3 | 0 | SHA | Unknown error


## Troubleshooting

- If you get `[Errno 13] Permission denied: '/dev/ttyACM0'`, adding yourself to the `dialout` group and rebooting should fix.
- If you get `usb.core.USBError: [Errno 13] Access denied (insufficient permissions)` you'll need to add a udev rule for the device, see [this stackoverflow post](https://stackoverflow.com/questions/53125118/why-is-python-pyusb-usb-core-access-denied-due-to-permissions-and-why-wont-the). This worked for me:
Expand All @@ -150,27 +183,20 @@ See the examples for more details.

- the device can get out of sync quite easily when something goes wrong. If so, turn it off and on again ;)

### Testing

Just run:

```sh
pytest
```

## Examples

### 0. Hash file
### Hash file

This just prints the hash of itself.
This script just prints the hash of itself.

```sh
python examples/hash_file.py
```

### 1. Decrypt data
### Encrypt/decrypt data

This example will look for an encrypted version of the data (examples/dataframe.csv). If not found it will encrypt the plaintext.
This example will look for an encrypted version of the data (examples/dataframe.csv). If not found it will first encrypt the plaintext.

Then it decrypts the ciphertext and loads the data into a pandas dataframe (you may need to install pandas).

Expand Down Expand Up @@ -200,7 +226,7 @@ decryption took 2.56s

If you now switch to a different device, it won't be able to decrypt the ciphertext and will return garbage.

### 3. Sign data
### Sign data

This example will compute a hash (SHA256) of a file and sign it. It outputs a json object containing the filename, the hash, the signature, and the device's public key.

Expand All @@ -215,7 +241,7 @@ signing/verifying took 0.55s
signature written to signature.json
```

### 3. Verify data
### Verify data

The signature data above should be verifiable by any ECDSA validation algorithm, but you can use the device for this. First it verifies the supplied hash corresponds to the file, then it verifies the signature against the hash and the given public key. It also prints whether the public key provided matches it's own public key.

Expand All @@ -238,3 +264,17 @@ verifying device is not the signing device
signature is valid
verifying took 0.79s
```

### Change PIN

```sh
python examples/change_pin.py
```

This just runs the PIN reset process:

- initialise device
- reset device (you'll need to enter the old PIN, even if this was set in the env)
- enter new PIN and repeat to confirm
- write new PIN to device
- reset device and initialise with new PIN
14 changes: 14 additions & 0 deletions examples/change_pin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
Example: change the device pin.
"""

from pico_crypto_key import CryptoKey


def change_pin() -> None:
with CryptoKey() as crypto_key:
crypto_key.set_pin()


if __name__ == "__main__":
change_pin()
10 changes: 4 additions & 6 deletions examples/decrypt_data.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
"""
Example 1: use device to read an encrypted dataset into a pandas dataframe.
Example: use device to encrypt a dataset (if not already present) and read an encrypted dataset into a pandas dataframe.
"""

import os
from io import BytesIO
from time import time

import pandas as pd # type: ignore
import toml

from pico_crypto_key import CryptoKey


def read_encrypted_dataframe(ciphertext: str, device_pin: str) -> None:
with CryptoKey(pin=device_pin) as crypto_key:
def read_encrypted_dataframe(ciphertext: str) -> None:
with CryptoKey() as crypto_key:
ciphertext = filename
# if the encrypted data isn't there, create it from the plaintext
if not os.path.isfile(ciphertext):
Expand All @@ -34,5 +33,4 @@ def read_encrypted_dataframe(ciphertext: str, device_pin: str) -> None:

if __name__ == "__main__":
filename = "examples/dataframe.csv.enc"
config = toml.load("./pyproject.toml")["pico"]["run"]
read_encrypted_dataframe(filename, config["PICO_CRYPTO_KEY_PIN"])
read_encrypted_dataframe(filename)
15 changes: 8 additions & 7 deletions examples/hash_file.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""
Example 0: compute SHA256 hash of a file.
Example: compute SHA256 hash of a file.
"""

import toml

from pico_crypto_key import CryptoKey

if __name__ == "__main__":
config = toml.load("./pyproject.toml")["pico"]["run"]

with CryptoKey(pin=config["PICO_CRYPTO_KEY_PIN"]) as crypto_key:
digest = crypto_key.hash(__file__)
def hash_file(file: str) -> None:
with CryptoKey() as crypto_key:
digest = crypto_key.hash(file)
print(f"{__file__}: {digest.hex()}")


if __name__ == "__main__":
hash_file(__file__)
14 changes: 5 additions & 9 deletions examples/sign_data.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
"""
Example 2: Use device to sign a dataset, returning (in json format) the hash,
Example: Use device to sign a dataset, returning (in json format) the hash,
signature and public key (as hex strings) for verification.
"""

import json
import time

import toml

from pico_crypto_key import CryptoKey


def sign_data(filename: str, device_pin: str) -> None:
with CryptoKey(pin=device_pin) as device:
def sign_data(filename: str) -> None:
with CryptoKey() as device:
start = time.time()
pubkey = device.pubkey()
digest, signature = device.sign(filename)
print("signing/verifying took %.2fs" % (time.time() - start))
print("signing took %.2fs" % (time.time() - start))

result = dict(
file=filename,
Expand All @@ -31,6 +29,4 @@ def sign_data(filename: str, device_pin: str) -> None:

if __name__ == "__main__":
filename = "./examples/dataframe.csv"
config = toml.load("./pyproject.toml")["pico"]["run"]

sign_data(filename, config["PICO_CRYPTO_KEY_PIN"])
sign_data(filename)
Loading

0 comments on commit f9bd708

Please sign in to comment.