├── test ├── nanopass.py ├── automaton.py └── test_app.py ├── ledger_app.toml ├── doc ├── demo.gif └── impl.md ├── key_14x14.gif ├── key_16x16.gif ├── nanopass.png ├── .cargo └── config.toml ├── Cargo.toml ├── src ├── tinyaes.rs ├── c │ ├── aes.h │ └── aes.c ├── password.rs └── main.rs ├── .github └── workflows │ └── rust.yml ├── README.md ├── nanopass.svg ├── nanopass.py └── LICENSE.md /test/nanopass.py: -------------------------------------------------------------------------------- 1 | ../nanopass.py -------------------------------------------------------------------------------- /ledger_app.toml: -------------------------------------------------------------------------------- 1 | [rust-app] 2 | manifest-path = "./Cargo.toml" 3 | -------------------------------------------------------------------------------- /doc/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/rust-app-password-manager/HEAD/doc/demo.gif -------------------------------------------------------------------------------- /key_14x14.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/rust-app-password-manager/HEAD/key_14x14.gif -------------------------------------------------------------------------------- /key_16x16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/rust-app-password-manager/HEAD/key_16x16.gif -------------------------------------------------------------------------------- /nanopass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LedgerHQ/rust-app-password-manager/HEAD/nanopass.png -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.nanos] 2 | runner = "speculos.py -m nanos" 3 | 4 | [unstable] 5 | build-std = ["core"] 6 | build-std-features = ["compiler-builtins-mem"] -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nanopass" 3 | version = "1.2.1" 4 | authors = ["yhql", "Olivier Hériveaux"] 5 | edition = "2021" 6 | build = "build.rs" 7 | 8 | [build-dependencies] 9 | cc = "1.0.73" 10 | bindgen = "0.59.2" 11 | 12 | [dependencies] 13 | nanos_sdk = { git = "https://github.com/LedgerHQ/ledger-nanos-sdk.git" } 14 | nanos_ui = { git = "https://github.com/LedgerHQ/ledger-nanos-ui.git" } 15 | cty = "0.2.0" 16 | heapless = { version = "0.7.16", default-features = false } 17 | 18 | [profile.release] 19 | opt-level = 's' 20 | lto = true 21 | 22 | [package.metadata.nanos] 23 | api_level = "1" 24 | name = "NanoPass" 25 | curve = ["secp256k1"] 26 | flags = "0" 27 | icon = "key_16x16.gif" 28 | icon_small = "key_14x14.gif" 29 | path = [""] 30 | -------------------------------------------------------------------------------- /src/tinyaes.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Ledger SAS 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![allow(non_snake_case)] 16 | #![allow(non_upper_case_globals)] 17 | #![allow(non_camel_case_types)] 18 | #![allow(dead_code)] 19 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 20 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | inputs: 10 | name: 11 | description: 'Manually triggered' 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | build_clippy_fmt: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: arm-none-eabi-gcc 21 | uses: fiam/arm-none-eabi-gcc@v1.0.3 22 | with: 23 | release: '9-2019-q4' 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | - name: Checkout SDK (targets) 27 | uses: actions/checkout@v3 28 | with: 29 | repository: 'LedgerHQ/ledger-nanos-sdk' 30 | path: rsdk 31 | - name: Install clang 32 | run: sudo apt-get update && sudo apt install -y clang 33 | - uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: nightly 36 | override: true 37 | components: rust-src, rustfmt, clippy 38 | - name: Cargo build 39 | uses: actions-rs/cargo@v1 40 | with: 41 | command: build 42 | args: -Z build-std=core -Z build-std-features=compiler-builtins-mem --target ./rsdk/nanos.json 43 | - run: rustup component add clippy 44 | - name: Cargo clippy 45 | uses: actions-rs/cargo@v1 46 | with: 47 | command: clippy 48 | args: -Z build-std=core -Z build-std-features=compiler-builtins-mem --target ./rsdk/nanos.json 49 | - run: rustup component add rustfmt 50 | - name: Cargo fmt 51 | uses: actions-rs/cargo@v1 52 | with: 53 | command: fmt 54 | args: --all -- --check 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Password Manager for Ledger Devices 2 | 3 | A simple password manager application for Ledger devices, with command-line 4 | interface similar to Unix pass. Works on Nano S, Nano S Plus and Nano X. 5 | 6 | Up to 128 login/password entries can be stored. Passwords can be exported 7 | encrypted to a file, and imported on another device sharing the same seed. 8 | 9 | ![Demo animation](doc/demo.gif) 10 | 11 | This application is written in Rust and C. 12 | 13 | [Implementation details](doc/impl.md) 14 | 15 | ## Usage 16 | 17 | The client script `nanopass.py` must be used to interact with the application 18 | to: 19 | - list stored passwords, 20 | - retrieve passwords, 21 | - insert or generate new passwords, 22 | - update or delete passwords, 23 | - export passwords to a JSON file, 24 | - import passwords from a JSON file. 25 | 26 | The application can also be used with the dedicated [chrome extension](https://github.com/LedgerHQ/nanopass-chrome-ext). 27 | 28 | ## Prerequisites 29 | 30 | * Install [cargo-ledger](https://github.com/LedgerHQ/cargo-ledger): `cargo install --git https://github.com/LedgerHQ/cargo-ledger` 31 | * Run `cargo ledger setup` 32 | * Install `arm-none-eabi-gcc` from 33 | 34 | ## Building and installing 35 | 36 | You can use 37 | [cargo-ledger](https://github.com/ledgerhq/cargo-ledger) which 38 | builds, outputs a `hex` file and a manifest file for `ledgerctl`, and loads it 39 | on a device in a single `cargo ledger build nanos --load` command in your app directory. 40 | 41 | ## License 42 | 43 | Licensed under Apache-2.0 license. 44 | 45 | [Tiny-AES](https://github.com/kokke/tiny-AES-c) library is included and is under 46 | The Unlicense. 47 | -------------------------------------------------------------------------------- /test/automaton.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2020 Ledger SAS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from nanopass import Client 18 | from time import sleep 19 | from binascii import hexlify 20 | import socket 21 | import os.path 22 | from speculos.client import SpeculosClient 23 | 24 | class Automaton(SpeculosClient): 25 | def __init__(self): 26 | app_path = os.path.join( 27 | os.path.dirname(__file__), "..", "target", "thumbv6m-none-eabi", 28 | "release", "nanopass") 29 | super().__init__(app_path) 30 | self.actions = '' 31 | self.cla = 0x80 32 | 33 | def apdu_exchange(self, ins: int, data: bytes=b"", p1: int=0, p2:int=0 34 | ) -> bytes: 35 | """ 36 | Send an APDU and return the response. Process button press indicated in 37 | self.actions during the APDU processing of the device. 38 | API matches ledgerwallet library. 39 | """ 40 | apdu = bytes([self.cla, ins, p1, p2, len(data)]) + data 41 | with super().apdu_exchange_nowait(self.cla, ins, data, p1=p1, p2=p2) as response: 42 | while len(self.actions): 43 | # Wait to be sure speculos has enough time to process 44 | sleep(0.1) 45 | c = self.actions[0] 46 | self.actions = self.actions[1:] 47 | if c == 'r': 48 | self.press_right() 49 | elif c == 'l': 50 | self.press_left() 51 | elif c == 'b': 52 | self.press_both() 53 | elif c == ';': 54 | # Next actions for next APDU 55 | break; 56 | return response.receive() 57 | 58 | def press(self, button: str): 59 | self.press_and_release(button) 60 | 61 | def press_left(self): 62 | self.press("left") 63 | 64 | def press_right(self): 65 | self.press("right") 66 | 67 | def press_both(self): 68 | self.press("both") 69 | 70 | -------------------------------------------------------------------------------- /src/c/aes.h: -------------------------------------------------------------------------------- 1 | #ifndef _AES_H_ 2 | #define _AES_H_ 3 | 4 | #include 5 | 6 | // #define the macros below to 1/0 to enable/disable the mode of operation. 7 | // 8 | // CBC enables AES encryption in CBC-mode of operation. 9 | // CTR enables encryption in counter-mode. 10 | // ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously. 11 | 12 | // The #ifndef-guard allows it to be configured before #include'ing or at compile time. 13 | #ifndef CBC 14 | #define CBC 1 15 | #endif 16 | 17 | #ifndef ECB 18 | #define ECB 1 19 | #endif 20 | 21 | #ifndef CTR 22 | #define CTR 1 23 | #endif 24 | 25 | 26 | //#define AES128 1 27 | //#define AES192 1 28 | #define AES256 1 29 | 30 | #define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only 31 | 32 | #if defined(AES256) && (AES256 == 1) 33 | #define AES_KEYLEN 32 34 | #define AES_keyExpSize 240 35 | #elif defined(AES192) && (AES192 == 1) 36 | #define AES_KEYLEN 24 37 | #define AES_keyExpSize 208 38 | #else 39 | #define AES_KEYLEN 16 // Key length in bytes 40 | #define AES_keyExpSize 176 41 | #endif 42 | 43 | struct AES_ctx 44 | { 45 | uint8_t RoundKey[AES_keyExpSize]; 46 | #if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) 47 | uint8_t Iv[AES_BLOCKLEN]; 48 | #endif 49 | }; 50 | 51 | void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key); 52 | #if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) 53 | void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv); 54 | void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv); 55 | #endif 56 | 57 | #if defined(ECB) && (ECB == 1) 58 | // buffer size is exactly AES_BLOCKLEN bytes; 59 | // you need only AES_init_ctx as IV is not used in ECB 60 | // NB: ECB is considered insecure for most uses 61 | void AES_ECB_encrypt(const struct AES_ctx* ctx, uint8_t* buf); 62 | void AES_ECB_decrypt(const struct AES_ctx* ctx, uint8_t* buf); 63 | 64 | #endif // #if defined(ECB) && (ECB == !) 65 | 66 | 67 | #if defined(CBC) && (CBC == 1) 68 | // buffer size MUST be mutile of AES_BLOCKLEN; 69 | // Suggest https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme 70 | // NOTES: you need to set IV in ctx via AES_init_ctx_iv() or AES_ctx_set_iv() 71 | // no IV should ever be reused with the same key 72 | void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, uint32_t length); 73 | void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, uint32_t length); 74 | 75 | #endif // #if defined(CBC) && (CBC == 1) 76 | 77 | 78 | #if defined(CTR) && (CTR == 1) 79 | 80 | // Same function for encrypting as for decrypting. 81 | // IV is incremented for every block, and used after encryption as XOR-compliment for output 82 | // Suggesting https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme 83 | // NOTES: you need to set IV in ctx with AES_init_ctx_iv() or AES_ctx_set_iv() 84 | // no IV should ever be reused with the same key 85 | void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, uint32_t length); 86 | 87 | #endif // #if defined(CTR) && (CTR == 1) 88 | 89 | 90 | #endif // _AES_H_ 91 | -------------------------------------------------------------------------------- /nanopass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 🗝 76 | -------------------------------------------------------------------------------- /test/test_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2020 Ledger SAS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from automaton import Automaton 18 | from nanopass import Client 19 | import random 20 | 21 | def subtest_password_list(client, passwords): 22 | """ Test password name listing. """ 23 | entries = client.get_names() 24 | assert (set(client.get_names()) == 25 | set(name for (name, _, _) in passwords)) 26 | 27 | def subtest_has_name(client, passwords): 28 | """ Test the HasName APDU command """ 29 | for name, _, _ in passwords: 30 | assert client.has_name(name) 31 | assert not client.has_name("undefined") 32 | 33 | def subtest_password_retrieval(client, auto, passwords): 34 | """ Verify the correctness of login and password values. """ 35 | for name, login, password in passwords: 36 | auto.actions = "rb" 37 | login2, password2 = client.get_by_name(name) 38 | assert login == login2 39 | assert password == password2 40 | 41 | def subtest_clear(client, auto, passwords): 42 | for name, login, password in passwords: 43 | auto.actions = "rb" 44 | client.add(name, login, password) 45 | assert client.get_size() == len(passwords) 46 | auto.actions = "bb" 47 | client.clear() 48 | assert client.get_size() == 0 49 | 50 | def test_app(): 51 | passwords = [ 52 | ("x", "", "1"), 53 | ("want", "a", "epuu7Aeja9"), 54 | ("emerge", "bamboo", "zexae2Moo2"), 55 | ("question", "predict", "dahTho9Thai5yiasie1c"), 56 | ("quick fiber estate ripple phrase", "topic", "huu4aeju2gooth1iS6ai") 57 | ] 58 | 59 | auto = Automaton() 60 | client = Client(auto) 61 | 62 | # Test password insertion 63 | assert client.get_size() == 0 64 | for i, (name, login, password) in enumerate(passwords): 65 | auto.actions = "rb" 66 | client.add(name, login, password) 67 | assert client.get_size() == i+1 68 | 69 | subtest_password_list(client, passwords) 70 | subtest_has_name(client, passwords) 71 | subtest_password_retrieval(client, auto, passwords) 72 | 73 | # Export in plain text and also in encrypted form 74 | # Do this before password removal testing 75 | auto.actions = "brb" 76 | export_plain = client.export(encrypt=False) 77 | auto.actions = "b" 78 | export_encrypted = client.export() 79 | 80 | # Test password removal 81 | removal_order = [name for name, _, _ in passwords] 82 | random.shuffle(removal_order) 83 | names = set(removal_order) 84 | for name in removal_order: 85 | auto.actions = "rb" 86 | client.delete_by_name(name) 87 | names.remove(name) 88 | assert set(client.get_names()) == names 89 | assert client.get_size() == 0 90 | 91 | # Test import plain 92 | subtest_clear(client, auto, passwords) 93 | auto.actions = ";b" 94 | client.import_("1.1.0", export_plain, encrypted=False) 95 | subtest_password_list(client, passwords) 96 | subtest_password_retrieval(client, auto, passwords) 97 | 98 | # Test import encrypted 99 | subtest_clear(client, auto, passwords) 100 | auto.actions = ";b" 101 | client.import_("1.1.0", export_encrypted, encrypted=True) 102 | subtest_password_list(client, passwords) 103 | subtest_password_retrieval(client, auto, passwords) 104 | 105 | -------------------------------------------------------------------------------- /src/password.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Ledger SAS 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// A basic class to store strings are fixed-size arrays. 16 | /// Storing null characters is not allowed (null is reserved to detect the 17 | /// end of the string). The stored string is not null terminated in the case 18 | /// all the array is used to store characters. 19 | #[derive(Clone, Copy)] 20 | pub struct ArrayString { 21 | bytes: [u8; N], 22 | } 23 | 24 | impl ArrayString { 25 | /// Create an empty string 26 | pub const fn new() -> ArrayString { 27 | ArrayString { bytes: [0; N] } 28 | } 29 | 30 | /// Set the string from an array of bytes. 31 | /// 32 | /// # Arguments 33 | /// 34 | /// * `bytes` - Array of bytes. Max size is N. The string must not have null 35 | /// bytes, but the last bytes of the array can be null (zero padding). 36 | pub fn set_from_bytes(&mut self, bytes: &[u8]) { 37 | let mut len = bytes.len(); 38 | while (len > 0) && (bytes[len - 1]) == 0 { 39 | len -= 1; 40 | } 41 | assert!(len <= N); 42 | self.bytes[..len].copy_from_slice(&bytes[..len]); 43 | for i in len..N { 44 | self.bytes[i] = 0; 45 | } 46 | } 47 | 48 | /// Returns an ArrayString initialized from bytes. 49 | /// 50 | /// # Arguments 51 | /// 52 | /// * `bytes` - Array of bytes. Max size is N. Must not have null bytes. 53 | pub fn from_bytes(bytes: &[u8]) -> ArrayString { 54 | let mut result = ArrayString::new(); 55 | result.set_from_bytes(bytes); 56 | result 57 | } 58 | 59 | /// Number of bytes in the string. 60 | pub fn len(&self) -> usize { 61 | let mut size = N; 62 | while (size > 0) && (self.bytes[size - 1] == 0) { 63 | size -= 1; 64 | } 65 | size 66 | } 67 | 68 | /// Return the bytes, non-mutable! 69 | pub fn bytes(&self) -> &[u8; N] { 70 | &self.bytes 71 | } 72 | 73 | /// Return the bytes as a str 74 | pub fn as_str(&self) -> &str { 75 | core::str::from_utf8(&self.bytes[..self.len()]).unwrap() 76 | } 77 | } 78 | 79 | impl core::cmp::PartialEq for ArrayString { 80 | fn eq(&self, other: &Self) -> bool { 81 | let len = self.len(); 82 | if other.len() != len { 83 | return false; 84 | } 85 | self.bytes[..len] == other.bytes[..len] 86 | } 87 | } 88 | 89 | impl Eq for ArrayString {} 90 | 91 | /// Storage for a password. 92 | /// 93 | /// This is intended to be stored in the Flash memory: 94 | /// - members have fixed size 95 | /// - total size of PasswordItem should be a multiple of Flash page size (here 96 | /// 64). 97 | /// 98 | /// As name and size are fixed arrays, we consider stored strings are padded 99 | /// with zeros. This is not null terminated, and UTF8 is allowed. 100 | #[derive(Clone, Copy)] 101 | pub struct PasswordItem { 102 | pub name: ArrayString<32>, 103 | pub login: ArrayString<32>, 104 | pub pass: ArrayString<32>, 105 | } 106 | 107 | impl PasswordItem { 108 | pub const fn new() -> PasswordItem { 109 | PasswordItem { 110 | name: ArrayString::new(), 111 | login: ArrayString::new(), 112 | pass: ArrayString::new(), 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /doc/impl.md: -------------------------------------------------------------------------------- 1 | # Implementation 2 | 3 | ## Password storage 4 | 5 | Passwords are stored in the non-volatile Flash memory of the device, in the 6 | application space. The manager can store up to 64 passwords (this can be 7 | increased in the source code, at the cost of application size). Each password is 8 | stored on an individual 64 bytes flash page, to allow atomic insertion and 9 | deletion (see `nvm::Collection` in the NanoS Rust SDK). 10 | 11 | Each 64 bytes password entry is divided in two 32 bytes blocks: one for the 12 | password name, and the other for the password itself. The name and password can 13 | be of variable size, up to 32 bytes each. The 32 bytes blocks are padded with 14 | zeros. ASCII characters should be stored (UTF8 may work except for the last 15 | character if it ends with a null byte). 16 | 17 | ## APDUs 18 | 19 | All APDUs use the class `0x80`. 20 | 21 | | INS | Name | Description | 22 | |------|-------------------------------|-------------------------------------------------------------| 23 | | 0x01 | [GetVersion](#getversion) | Returns version string | 24 | | 0x02 | [GetSize](#getsize) | Returns the number of stored passwords | 25 | | 0x03 | [Add](#add) | Add a new password | 26 | | 0x04 | [GetName](#getname) | Returns name of the n-th password | 27 | | 0x05 | [GetByName](#getbyname) | Return the password with the given name | 28 | | 0x06 | [DeleteByName](#deletebyname) | Delete the password with the given name | 29 | | 0x07 | [Export](#export) | Start password export procedure | 30 | | 0x08 | [ExportNext](#exportnext) | Export the next password | 31 | | 0x09 | [Import](#import) | Start password import procedure | 32 | | 0x0a | [ImportNext](#importnext) | Import the next password | 33 | | 0x0b | [Clear](#clear) | Remove all passwords | 34 | | 0x0c | [Quit](#quit) | Quit application | 35 | | 0x0d | [ShowOnScreen](#showonscreen) | Show the password with the given name on the screen | 36 | | 0x0e | [HasName](#hasname) | Indicate if a password with the given name is stored or not | 37 | 38 | ## GetVersion 39 | 40 | Returns version string, for instance "1.0.0". 41 | 42 | ## GetSize 43 | 44 | Returns the number of passwords stored. 45 | User consent is not required for this operation. 46 | 47 | The device sends the result encoded in big-endian with 4-bytes. 48 | 49 | ## Add 50 | 51 | Add a new password in the internal collection. 52 | This operation requires user consent. 53 | 54 | The P1 field can be: 55 | - 0: password is passed in the Data field 56 | - 1: password is randomly generated by the device. 57 | 58 | The Data field of the command must have the 32 bytes for the 59 | name (padded with zeros), 32 bytes for the login (padded with zeros) and 32 60 | bytes for the password (padded with zeros) if device generation is not 61 | requested. 62 | 63 | ## GetName 64 | 65 | Returns the name of the n-th password stored. 66 | User consent is not required for this operation. 67 | 68 | The Data field of the APDU must contain the password index, encoded in 69 | big-endian with 4 bytes 70 | 71 | ## GetByName 72 | 73 | Returns the password with the given name. 74 | This operation requires user consent. 75 | 76 | The Data field of the APDU must contain the password name on 32-bytes (padded 77 | with zeros). 78 | 79 | The device responds with the 32-bytes password (padded with zeros). 80 | 81 | ## DeleteByName 82 | 83 | Delete the password with the given name. 84 | This operation requires user consent. 85 | 86 | The Data field of the APDU must contain the password name on 32-bytes (padded 87 | with zeros). 88 | 89 | ## Export 90 | 91 | Starts export procedure. 92 | This operation requires user consent. 93 | 94 | The P1 field can be: 95 | - 0: passwords are exported in plaintext. A warning message is displayed to the 96 | user. Any device can import the returned data. 97 | - 1: passwords are returned encrypted and MACed. Only a device with the same 98 | seed can import the returned data. 99 | 100 | This command returns the number of password entries that will be exported, 101 | encoded in big-endian with 4 bytes. 102 | Once the export procedure has been started, each password must be retrieved 103 | with the ExportNext command. The export procedure ends when all passwords have 104 | been readout. 105 | 106 | ## ExportNext 107 | 108 | Export the next password during the export procedure. 109 | User consent is not required for this operation (verified during Export). 110 | 111 | If plaintext mode is selected, the device responds with the name and password 112 | blocks (2 * 32 bytes) in plaintext. 113 | 114 | If encrypted export mode is selected, the device responds with a 16-bytes nonce, 115 | followed by encrypted name and password, and finally a 16-bytes MAC used to 116 | verify integrity during import. 117 | 118 | ## Import 119 | 120 | Starts the import procedure. 121 | This operation requires user consent. 122 | 123 | The P1 field can be: 124 | - 0: passwords are imported in plaintext. 125 | - 1: passwords are imported encrypted and MAC is verified. 126 | 127 | The Data field must contain the number of passwords to be imported, encoded in 128 | big-endian with 4 bytes. 129 | 130 | Once the import procedure has been started, each password must be imported with 131 | the ImportNext command. The import procedure ends when all passwords have been 132 | imported, or if a MAC verification fails. 133 | 134 | ## ImportNext 135 | 136 | Import the next password during the import procedure. 137 | User consent is not required for this operation (verified during Import). 138 | 139 | The Data field must contain the Data blob received during export. It can be in 140 | plaintext if the plaintext mode has been selected, or encrypted. 141 | 142 | ## Clear 143 | 144 | Remove all password. 145 | User consent is not required for this operation. 146 | 147 | ## Quit 148 | 149 | Quit application to return to the dashboard. 150 | User consent is not required for this operation. 151 | 152 | ## ShowOnScreen 153 | 154 | Display on the device the password with the given name. 155 | This operation requires user consent. 156 | 157 | The Data field of the APDU must contain the password name on 32-bytes (padded 158 | with zeros). 159 | 160 | ## HasName 161 | 162 | Tell if a password with the given name exists. 163 | This operation does not require user consent. 164 | 165 | The Data field of the APDU must contain the password name on 32-bytes (padded 166 | with zeros). 167 | 168 | The response data field is one byte long. The response byte is 0x01 if the 169 | password exists, 0x00 otherwise. 170 | -------------------------------------------------------------------------------- /nanopass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2020 Ledger SAS 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | from random import getrandbits as rnd 18 | from binascii import hexlify, unhexlify 19 | import click 20 | import binascii 21 | import json 22 | from typing import Optional, List, Tuple 23 | import ledgerwallet.client 24 | 25 | MAX_NAME_LEN = 32 26 | MAX_LOGIN_LEN = 32 27 | MAX_PASS_LEN = 32 28 | 29 | class BadVersion(Exception): 30 | pass 31 | 32 | def str_to_bytes_pad(s, size): 33 | result = bytearray(s.encode()) 34 | assert len(result) <= size 35 | while len(result) < size: 36 | result.append(0) 37 | return result 38 | 39 | def bytes_to_str(data): 40 | while (len(data) > 0) and (data[-1] == 0): 41 | data = data[:-1] 42 | return data.decode() 43 | 44 | class Client: 45 | def __init__(self, dev): 46 | """ 47 | Connects to a device. 48 | :param dev: Instance which implements the communication with the device. 49 | """ 50 | self.dev = dev 51 | self.dev.cla = 0x80 52 | 53 | def open_app(self): 54 | app_name = "nanopass".encode() 55 | self.dev.cla = 0xe0 56 | try: 57 | # Due to current limitation, app won't reply to this APDU and an 58 | # OSError is thrown as the USB device disconnects. Handle it 59 | # silently. 60 | # This needs to be improved 61 | self.dev.apdu_exchange(0xd8, app_name) 62 | finally: 63 | self.dev.cla = 0x80 64 | 65 | def quit_app(self): 66 | try: 67 | self.dev.apdu_exchange(0x0c) 68 | except OSError as e: 69 | pass 70 | 71 | def get_version(self) -> str: 72 | """ :return: App version string """ 73 | resp = self.dev.apdu_exchange(0x01) 74 | offset = 0 75 | assert resp[offset] == 1 # Check format 76 | offset += 1 77 | length = resp[offset] # App name length 78 | offset += 1 79 | value = resp[offset:offset+length] 80 | assert value == b"nanopass" 81 | offset += length 82 | length = resp[offset] # App version string length 83 | offset += 1 84 | value = resp[offset:offset+length] 85 | return value.decode() 86 | 87 | def get_size(self) -> int: 88 | """ 89 | :return: Number of password entries. 90 | """ 91 | resp = self.dev.apdu_exchange(0x02) 92 | assert len(resp) == 4 93 | return int.from_bytes(resp, 'big') 94 | 95 | def add(self, name: str, login: str, password: Optional[str] = None): 96 | """ 97 | Add a new password. 98 | :param name: Password name. 99 | :param login: Password login. 100 | :param password: Password. None if it is generated by the device. 101 | """ 102 | name_bytes = str_to_bytes_pad(name, MAX_NAME_LEN) 103 | login_bytes = str_to_bytes_pad(login, MAX_LOGIN_LEN) 104 | if password is not None: 105 | p1 = 0x00 106 | password_bytes = str_to_bytes_pad(password, MAX_PASS_LEN) 107 | else: 108 | p1 = 0x01 109 | password_bytes = bytearray() 110 | self.dev.apdu_exchange(0x03, p1=p1, data=name_bytes + login_bytes + 111 | password_bytes) 112 | 113 | def get_name(self, index: int) -> str: 114 | """ 115 | Retrieve name of a password 116 | :param index: Password entry index 117 | :return: Name 118 | """ 119 | r = self.dev.apdu_exchange(0x04, index.to_bytes(4, 'big')) 120 | assert len(r) == 32 121 | return bytes_to_str(r) 122 | 123 | def get_names(self) -> List[str]: 124 | """ :return: List of password names """ 125 | return [self.get_name(i) for i in range(self.get_size())] 126 | 127 | def get_by_name(self, name: str) -> Tuple[str, str]: 128 | """ 129 | Retrieve the password with the given name. 130 | :param name: Password name. 131 | :return: Login and Password string tuple. 132 | """ 133 | name_bytes = str_to_bytes_pad(name, MAX_NAME_LEN) 134 | r = self.dev.apdu_exchange(0x05, name_bytes) 135 | login = bytes_to_str(r[:32]) 136 | password = bytes_to_str(r[32:32+64]) 137 | return (login, password) 138 | 139 | def get_by_name_internal(self, name: str): 140 | """ 141 | Ask the device to display on screen the login and password with the 142 | given name. Using this method, no sensitive information is transfered to 143 | the computer. 144 | :param name: Password name. 145 | """ 146 | name_bytes = str_to_bytes_pad(name, MAX_NAME_LEN) 147 | self.dev.apdu_exchange(0x0d, name_bytes) 148 | 149 | def delete_by_name(self, name: str): 150 | """ 151 | Remove a password. 152 | :param name: Password name. 153 | """ 154 | name_bytes = str_to_bytes_pad(name, MAX_NAME_LEN) 155 | self.dev.apdu_exchange(0x06, name_bytes) 156 | 157 | def export(self, encrypt: bool=True) -> List[bytes]: 158 | """ 159 | Export passwords. 160 | :param encrypt: True to encrypt passwords during export, False to export 161 | in plaintext. 162 | :return: Exported entries. 163 | """ 164 | p1 = 0x01 165 | if not encrypt: 166 | p1 = 0x00 167 | count = int.from_bytes(self.dev.apdu_exchange(0x07, p1=p1), 'big') 168 | entries = [] 169 | for i in range(count): 170 | entries.append(self.dev.apdu_exchange(0x08)) 171 | return entries 172 | 173 | def import_(self, version, entries: List[bytes], encrypted: bool): 174 | """ 175 | Import password entries. 176 | :param version: Export file version, used for migration. 177 | :param entries: Password entries to be imported. 178 | :param encrypted: True if the entries are encrypted, False if it is in 179 | plaintext. 180 | """ 181 | # We don't support import on 1.0.0 anymore. 182 | # App must be upgraded. Password exports from 1.0.0 can be imported. 183 | if self.get_version() < "1.1.0": 184 | raise BadVersion("App version must be >= 1.1.0") 185 | # We cannot import files from 1.0.0 if they are encrypted. 186 | if encrypted and (version < "1.1.0"): 187 | raise BadVersion("Cannot import version < 1.1.0 encrypted exports") 188 | apdu = bytearray(b'\x80\x09\x00\x00\x04' + 189 | len(entries).to_bytes(4, 'big')) 190 | p1 = 0x00 191 | if encrypted: 192 | p1 = 0x01 193 | r = self.dev.apdu_exchange( 194 | 0x09, p1=p1, data=len(entries).to_bytes(4, 'big')) 195 | for p in entries: 196 | if version < "1.1.0": 197 | # Patch the data blob to add login 198 | assert encrypted == False 199 | p = p[:32] + (b"\x00" * 32) + p[32:64] 200 | assert len(p) == {True: 16+96+16, False: 96}[encrypted] 201 | self.dev.apdu_exchange(0x0a, p) 202 | 203 | def clear(self): 204 | """ Remove all passwords """ 205 | self.dev.apdu_exchange(0x0b) 206 | 207 | def has_name(self, name: str): 208 | """ Query if a password with the given name exists. """ 209 | name_bytes = str_to_bytes_pad(name, MAX_NAME_LEN) 210 | res = self.dev.apdu_exchange(0x0e, name_bytes) 211 | assert len(res) == 1 212 | assert res[0] in (0, 1) 213 | return bool(res[0]) 214 | 215 | 216 | @click.group() 217 | @click.pass_context 218 | def cli(ctx): 219 | ctx.ensure_object(dict) 220 | dev = ledgerwallet.client.LedgerClient() 221 | ctx.obj['DEV'] = Client(dev) 222 | 223 | @cli.command(help="Print installed application version") 224 | @click.pass_context 225 | def version(ctx): 226 | dev = ctx.obj['DEV'] 227 | print(dev.get_version()) 228 | 229 | @cli.command(help="Inserts a new password") 230 | @click.argument('name') 231 | @click.option('--login', default="") 232 | @click.pass_context 233 | def insert(ctx, name, login): 234 | password = input("Password (empty to generate):") 235 | if len(password) == 0: 236 | password = None 237 | print("Confirm password creation on your device...") 238 | dev = ctx.obj['DEV'] 239 | dev.add(name, login, password) 240 | 241 | @cli.command(help="Print a stored password") 242 | @click.pass_context 243 | @click.argument('name') 244 | def get(ctx, name): 245 | dev = ctx.obj['DEV'] 246 | if not dev.has_name(name): 247 | print("Credentials not found") 248 | return 249 | print("Confirm access on device...") 250 | login, password = dev.get_by_name(name) 251 | if len(login): 252 | print("login:", login) 253 | print("password:", password) 254 | 255 | @cli.command(help="Print a stored password on the device") 256 | @click.pass_context 257 | @click.argument('name') 258 | def getinternal(ctx, name): 259 | print("Confirm password display on your device...") 260 | dev = ctx.obj['DEV'] 261 | print(dev.get_by_name_internal(name)) 262 | 263 | @cli.command(help="List the names of stored passwords") 264 | @click.pass_context 265 | def list(ctx): 266 | dev = ctx.obj['DEV'] 267 | entries = dev.get_names() 268 | entries.sort() 269 | for entry in entries: 270 | print('-', entry) 271 | 272 | @cli.command(help="Remove a password from the store") 273 | @click.pass_context 274 | @click.argument('name') 275 | def remove(ctx, name): 276 | dev = ctx.obj['DEV'] 277 | dev.delete_by_name(name) 278 | 279 | @cli.command(help="Export passwords to JSON file") 280 | @click.argument('path') 281 | @click.option('--encrypt/--no-encrypt', default=True) 282 | @click.pass_context 283 | def export(ctx, path, encrypt): 284 | dev = ctx.obj['DEV'] 285 | entries = dev.export(encrypt) 286 | export = { 287 | 'version': dev.get_version(), 288 | 'encrypted': encrypt, 289 | 'entries': [binascii.hexlify(e).decode() for e in entries] 290 | } 291 | with open(path, 'wb') as f: 292 | f.write(json.dumps(export, indent=2).encode()) 293 | 294 | @cli.command(name='import', help="Import passwords from JSON file") 295 | @click.argument('path') 296 | @click.pass_context 297 | def import_(ctx, path): 298 | dev = ctx.obj['DEV'] 299 | data = json.loads(open(path, 'rb').read().decode()) 300 | entries = [bytes.fromhex(e) for e in data['entries']] 301 | encrypted = data['encrypted'] 302 | dev.import_(data['version'], entries, encrypted) 303 | 304 | @cli.command(help="Clear all passwords") 305 | @click.pass_context 306 | def clear(ctx): 307 | dev = ctx.obj['DEV'] 308 | dev.clear() 309 | 310 | @cli.command(name='open', help="Open application") 311 | @click.pass_context 312 | def open_(ctx): 313 | dev = ctx.obj['DEV'] 314 | dev.open_app() 315 | 316 | @cli.command(help="Quit application") 317 | @click.pass_context 318 | def quit(ctx): 319 | dev = ctx.obj['DEV'] 320 | dev.quit_app() 321 | 322 | 323 | if __name__ == '__main__': 324 | cli() 325 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/c/aes.c: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode. 4 | Block size can be chosen in aes.h - available choices are AES128, AES192, AES256. 5 | 6 | The implementation is verified against the test vectors in: 7 | National Institute of Standards and Technology Special Publication 800-38A 2001 ED 8 | 9 | ECB-AES128 10 | ---------- 11 | 12 | plain-text: 13 | 6bc1bee22e409f96e93d7e117393172a 14 | ae2d8a571e03ac9c9eb76fac45af8e51 15 | 30c81c46a35ce411e5fbc1191a0a52ef 16 | f69f2445df4f9b17ad2b417be66c3710 17 | 18 | key: 19 | 2b7e151628aed2a6abf7158809cf4f3c 20 | 21 | resulting cipher 22 | 3ad77bb40d7a3660a89ecaf32466ef97 23 | f5d3d58503b9699de785895a96fdbaaf 24 | 43b1cd7f598ece23881b00e3ed030688 25 | 7b0c785e27e8ad3f8223207104725dd4 26 | 27 | 28 | NOTE: String length must be evenly divisible by 16byte (str_len % 16 == 0) 29 | You should pad the end of the string with zeros if this is not the case. 30 | For AES192/256 the key size is proportionally larger. 31 | 32 | */ 33 | 34 | 35 | /*****************************************************************************/ 36 | /* Includes: */ 37 | /*****************************************************************************/ 38 | #include // CBC mode, for memset 39 | #include "aes.h" 40 | 41 | /*****************************************************************************/ 42 | /* Defines: */ 43 | /*****************************************************************************/ 44 | // The number of columns comprising a state in AES. This is a constant in AES. Value=4 45 | #define Nb 4 46 | 47 | #if defined(AES256) && (AES256 == 1) 48 | #define Nk 8 49 | #define Nr 14 50 | #elif defined(AES192) && (AES192 == 1) 51 | #define Nk 6 52 | #define Nr 12 53 | #else 54 | #define Nk 4 // The number of 32 bit words in a key. 55 | #define Nr 10 // The number of rounds in AES Cipher. 56 | #endif 57 | 58 | // jcallan@github points out that declaring Multiply as a function 59 | // reduces code size considerably with the Keil ARM compiler. 60 | // See this link for more information: https://github.com/kokke/tiny-AES-C/pull/3 61 | #ifndef MULTIPLY_AS_A_FUNCTION 62 | #define MULTIPLY_AS_A_FUNCTION 0 63 | #endif 64 | 65 | 66 | 67 | 68 | /*****************************************************************************/ 69 | /* Private variables: */ 70 | /*****************************************************************************/ 71 | // state - array holding the intermediate results during decryption. 72 | typedef uint8_t state_t[4][4]; 73 | 74 | 75 | 76 | // The lookup-tables are marked const so they can be placed in read-only storage instead of RAM 77 | // The numbers below can be computed dynamically trading ROM for RAM - 78 | // This can be useful in (embedded) bootloader applications, where ROM is often limited. 79 | static const uint8_t sbox[256] = { 80 | //0 1 2 3 4 5 6 7 8 9 A B C D E F 81 | 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 82 | 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 83 | 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 84 | 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 85 | 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 86 | 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 87 | 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 88 | 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 89 | 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 90 | 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 91 | 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 92 | 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 93 | 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 94 | 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 95 | 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 96 | 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 }; 97 | 98 | static const uint8_t rsbox[256] = { 99 | 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 100 | 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 101 | 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 102 | 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 103 | 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 104 | 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 105 | 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 106 | 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 107 | 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 108 | 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 109 | 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 110 | 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 111 | 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 112 | 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 113 | 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 114 | 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d }; 115 | 116 | // The round constant word array, Rcon[i], contains the values given by 117 | // x to the power (i-1) being powers of x (x is denoted as {02}) in the field GF(2^8) 118 | static const uint8_t Rcon[11] = { 119 | 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 }; 120 | 121 | /* 122 | * Jordan Goulder points out in PR #12 (https://github.com/kokke/tiny-AES-C/pull/12), 123 | * that you can remove most of the elements in the Rcon array, because they are unused. 124 | * 125 | * From Wikipedia's article on the Rijndael key schedule @ https://en.wikipedia.org/wiki/Rijndael_key_schedule#Rcon 126 | * 127 | * "Only the first some of these constants are actually used – up to rcon[10] for AES-128 (as 11 round keys are needed), 128 | * up to rcon[8] for AES-192, up to rcon[7] for AES-256. rcon[0] is not used in AES algorithm." 129 | */ 130 | 131 | 132 | /*****************************************************************************/ 133 | /* Private functions: */ 134 | /*****************************************************************************/ 135 | /* 136 | static uint8_t getSBoxValue(uint8_t num) 137 | { 138 | return sbox[num]; 139 | } 140 | */ 141 | #define getSBoxValue(num) (sbox[(num)]) 142 | /* 143 | static uint8_t getSBoxInvert(uint8_t num) 144 | { 145 | return rsbox[num]; 146 | } 147 | */ 148 | #define getSBoxInvert(num) (rsbox[(num)]) 149 | 150 | // This function produces Nb(Nr+1) round keys. The round keys are used in each round to decrypt the states. 151 | static void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key) 152 | { 153 | unsigned i, j, k; 154 | uint8_t tempa[4]; // Used for the column/row operations 155 | 156 | // The first round key is the key itself. 157 | for (i = 0; i < Nk; ++i) 158 | { 159 | RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; 160 | RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; 161 | RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; 162 | RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; 163 | } 164 | 165 | // All other round keys are found from the previous round keys. 166 | for (i = Nk; i < Nb * (Nr + 1); ++i) 167 | { 168 | { 169 | k = (i - 1) * 4; 170 | tempa[0]=RoundKey[k + 0]; 171 | tempa[1]=RoundKey[k + 1]; 172 | tempa[2]=RoundKey[k + 2]; 173 | tempa[3]=RoundKey[k + 3]; 174 | 175 | } 176 | 177 | if (i % Nk == 0) 178 | { 179 | // This function shifts the 4 bytes in a word to the left once. 180 | // [a0,a1,a2,a3] becomes [a1,a2,a3,a0] 181 | 182 | // Function RotWord() 183 | { 184 | const uint8_t u8tmp = tempa[0]; 185 | tempa[0] = tempa[1]; 186 | tempa[1] = tempa[2]; 187 | tempa[2] = tempa[3]; 188 | tempa[3] = u8tmp; 189 | } 190 | 191 | // SubWord() is a function that takes a four-byte input word and 192 | // applies the S-box to each of the four bytes to produce an output word. 193 | 194 | // Function Subword() 195 | { 196 | tempa[0] = getSBoxValue(tempa[0]); 197 | tempa[1] = getSBoxValue(tempa[1]); 198 | tempa[2] = getSBoxValue(tempa[2]); 199 | tempa[3] = getSBoxValue(tempa[3]); 200 | } 201 | 202 | tempa[0] = tempa[0] ^ Rcon[i/Nk]; 203 | } 204 | #if defined(AES256) && (AES256 == 1) 205 | if (i % Nk == 4) 206 | { 207 | // Function Subword() 208 | { 209 | tempa[0] = getSBoxValue(tempa[0]); 210 | tempa[1] = getSBoxValue(tempa[1]); 211 | tempa[2] = getSBoxValue(tempa[2]); 212 | tempa[3] = getSBoxValue(tempa[3]); 213 | } 214 | } 215 | #endif 216 | j = i * 4; k=(i - Nk) * 4; 217 | RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0]; 218 | RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1]; 219 | RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2]; 220 | RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3]; 221 | } 222 | } 223 | 224 | void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key) 225 | { 226 | KeyExpansion(ctx->RoundKey, key); 227 | } 228 | #if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) 229 | void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv) 230 | { 231 | KeyExpansion(ctx->RoundKey, key); 232 | memcpy (ctx->Iv, iv, AES_BLOCKLEN); 233 | } 234 | void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv) 235 | { 236 | memcpy (ctx->Iv, iv, AES_BLOCKLEN); 237 | } 238 | #endif 239 | 240 | // This function adds the round key to state. 241 | // The round key is added to the state by an XOR function. 242 | static void AddRoundKey(uint8_t round, state_t* state, const uint8_t* RoundKey) 243 | { 244 | uint8_t i,j; 245 | for (i = 0; i < 4; ++i) 246 | { 247 | for (j = 0; j < 4; ++j) 248 | { 249 | (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j]; 250 | } 251 | } 252 | } 253 | 254 | // The SubBytes Function Substitutes the values in the 255 | // state matrix with values in an S-box. 256 | static void SubBytes(state_t* state) 257 | { 258 | uint8_t i, j; 259 | for (i = 0; i < 4; ++i) 260 | { 261 | for (j = 0; j < 4; ++j) 262 | { 263 | (*state)[j][i] = getSBoxValue((*state)[j][i]); 264 | } 265 | } 266 | } 267 | 268 | // The ShiftRows() function shifts the rows in the state to the left. 269 | // Each row is shifted with different offset. 270 | // Offset = Row number. So the first row is not shifted. 271 | static void ShiftRows(state_t* state) 272 | { 273 | uint8_t temp; 274 | 275 | // Rotate first row 1 columns to left 276 | temp = (*state)[0][1]; 277 | (*state)[0][1] = (*state)[1][1]; 278 | (*state)[1][1] = (*state)[2][1]; 279 | (*state)[2][1] = (*state)[3][1]; 280 | (*state)[3][1] = temp; 281 | 282 | // Rotate second row 2 columns to left 283 | temp = (*state)[0][2]; 284 | (*state)[0][2] = (*state)[2][2]; 285 | (*state)[2][2] = temp; 286 | 287 | temp = (*state)[1][2]; 288 | (*state)[1][2] = (*state)[3][2]; 289 | (*state)[3][2] = temp; 290 | 291 | // Rotate third row 3 columns to left 292 | temp = (*state)[0][3]; 293 | (*state)[0][3] = (*state)[3][3]; 294 | (*state)[3][3] = (*state)[2][3]; 295 | (*state)[2][3] = (*state)[1][3]; 296 | (*state)[1][3] = temp; 297 | } 298 | 299 | static uint8_t xtime(uint8_t x) 300 | { 301 | return ((x<<1) ^ (((x>>7) & 1) * 0x1b)); 302 | } 303 | 304 | // MixColumns function mixes the columns of the state matrix 305 | static void MixColumns(state_t* state) 306 | { 307 | uint8_t i; 308 | uint8_t Tmp, Tm, t; 309 | for (i = 0; i < 4; ++i) 310 | { 311 | t = (*state)[i][0]; 312 | Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3] ; 313 | Tm = (*state)[i][0] ^ (*state)[i][1] ; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp ; 314 | Tm = (*state)[i][1] ^ (*state)[i][2] ; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp ; 315 | Tm = (*state)[i][2] ^ (*state)[i][3] ; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp ; 316 | Tm = (*state)[i][3] ^ t ; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp ; 317 | } 318 | } 319 | 320 | // Multiply is used to multiply numbers in the field GF(2^8) 321 | // Note: The last call to xtime() is unneeded, but often ends up generating a smaller binary 322 | // The compiler seems to be able to vectorize the operation better this way. 323 | // See https://github.com/kokke/tiny-AES-c/pull/34 324 | #if MULTIPLY_AS_A_FUNCTION 325 | static uint8_t Multiply(uint8_t x, uint8_t y) 326 | { 327 | return (((y & 1) * x) ^ 328 | ((y>>1 & 1) * xtime(x)) ^ 329 | ((y>>2 & 1) * xtime(xtime(x))) ^ 330 | ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ 331 | ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))); /* this last call to xtime() can be omitted */ 332 | } 333 | #else 334 | #define Multiply(x, y) \ 335 | ( ((y & 1) * x) ^ \ 336 | ((y>>1 & 1) * xtime(x)) ^ \ 337 | ((y>>2 & 1) * xtime(xtime(x))) ^ \ 338 | ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ \ 339 | ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))) \ 340 | 341 | #endif 342 | 343 | #if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) 344 | // MixColumns function mixes the columns of the state matrix. 345 | // The method used to multiply may be difficult to understand for the inexperienced. 346 | // Please use the references to gain more information. 347 | static void InvMixColumns(state_t* state) 348 | { 349 | int i; 350 | uint8_t a, b, c, d; 351 | for (i = 0; i < 4; ++i) 352 | { 353 | a = (*state)[i][0]; 354 | b = (*state)[i][1]; 355 | c = (*state)[i][2]; 356 | d = (*state)[i][3]; 357 | 358 | (*state)[i][0] = Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09); 359 | (*state)[i][1] = Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d); 360 | (*state)[i][2] = Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b); 361 | (*state)[i][3] = Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e); 362 | } 363 | } 364 | 365 | 366 | // The SubBytes Function Substitutes the values in the 367 | // state matrix with values in an S-box. 368 | static void InvSubBytes(state_t* state) 369 | { 370 | uint8_t i, j; 371 | for (i = 0; i < 4; ++i) 372 | { 373 | for (j = 0; j < 4; ++j) 374 | { 375 | (*state)[j][i] = getSBoxInvert((*state)[j][i]); 376 | } 377 | } 378 | } 379 | 380 | static void InvShiftRows(state_t* state) 381 | { 382 | uint8_t temp; 383 | 384 | // Rotate first row 1 columns to right 385 | temp = (*state)[3][1]; 386 | (*state)[3][1] = (*state)[2][1]; 387 | (*state)[2][1] = (*state)[1][1]; 388 | (*state)[1][1] = (*state)[0][1]; 389 | (*state)[0][1] = temp; 390 | 391 | // Rotate second row 2 columns to right 392 | temp = (*state)[0][2]; 393 | (*state)[0][2] = (*state)[2][2]; 394 | (*state)[2][2] = temp; 395 | 396 | temp = (*state)[1][2]; 397 | (*state)[1][2] = (*state)[3][2]; 398 | (*state)[3][2] = temp; 399 | 400 | // Rotate third row 3 columns to right 401 | temp = (*state)[0][3]; 402 | (*state)[0][3] = (*state)[1][3]; 403 | (*state)[1][3] = (*state)[2][3]; 404 | (*state)[2][3] = (*state)[3][3]; 405 | (*state)[3][3] = temp; 406 | } 407 | #endif // #if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) 408 | 409 | // Cipher is the main function that encrypts the PlainText. 410 | static void Cipher(state_t* state, const uint8_t* RoundKey) 411 | { 412 | uint8_t round = 0; 413 | 414 | // Add the First round key to the state before starting the rounds. 415 | AddRoundKey(0, state, RoundKey); 416 | 417 | // There will be Nr rounds. 418 | // The first Nr-1 rounds are identical. 419 | // These Nr rounds are executed in the loop below. 420 | // Last one without MixColumns() 421 | for (round = 1; ; ++round) 422 | { 423 | SubBytes(state); 424 | ShiftRows(state); 425 | if (round == Nr) { 426 | break; 427 | } 428 | MixColumns(state); 429 | AddRoundKey(round, state, RoundKey); 430 | } 431 | // Add round key to last round 432 | AddRoundKey(Nr, state, RoundKey); 433 | } 434 | 435 | #if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) 436 | static void InvCipher(state_t* state, const uint8_t* RoundKey) 437 | { 438 | uint8_t round = 0; 439 | 440 | // Add the First round key to the state before starting the rounds. 441 | AddRoundKey(Nr, state, RoundKey); 442 | 443 | // There will be Nr rounds. 444 | // The first Nr-1 rounds are identical. 445 | // These Nr rounds are executed in the loop below. 446 | // Last one without InvMixColumn() 447 | for (round = (Nr - 1); ; --round) 448 | { 449 | InvShiftRows(state); 450 | InvSubBytes(state); 451 | AddRoundKey(round, state, RoundKey); 452 | if (round == 0) { 453 | break; 454 | } 455 | InvMixColumns(state); 456 | } 457 | 458 | } 459 | #endif // #if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) 460 | 461 | /*****************************************************************************/ 462 | /* Public functions: */ 463 | /*****************************************************************************/ 464 | #if defined(ECB) && (ECB == 1) 465 | 466 | 467 | void AES_ECB_encrypt(const struct AES_ctx* ctx, uint8_t* buf) 468 | { 469 | // The next function call encrypts the PlainText with the Key using AES algorithm. 470 | Cipher((state_t*)buf, ctx->RoundKey); 471 | } 472 | 473 | void AES_ECB_decrypt(const struct AES_ctx* ctx, uint8_t* buf) 474 | { 475 | // The next function call decrypts the PlainText with the Key using AES algorithm. 476 | InvCipher((state_t*)buf, ctx->RoundKey); 477 | } 478 | 479 | 480 | #endif // #if defined(ECB) && (ECB == 1) 481 | 482 | 483 | 484 | 485 | 486 | #if defined(CBC) && (CBC == 1) 487 | 488 | 489 | static void XorWithIv(uint8_t* buf, const uint8_t* Iv) 490 | { 491 | uint8_t i; 492 | for (i = 0; i < AES_BLOCKLEN; ++i) // The block in AES is always 128bit no matter the key size 493 | { 494 | buf[i] ^= Iv[i]; 495 | } 496 | } 497 | 498 | void AES_CBC_encrypt_buffer(struct AES_ctx *ctx, uint8_t* buf, uint32_t length) 499 | { 500 | uintptr_t i; 501 | uint8_t *Iv = ctx->Iv; 502 | for (i = 0; i < length; i += AES_BLOCKLEN) 503 | { 504 | XorWithIv(buf, Iv); 505 | Cipher((state_t*)buf, ctx->RoundKey); 506 | Iv = buf; 507 | buf += AES_BLOCKLEN; 508 | } 509 | /* store Iv in ctx for next call */ 510 | memcpy(ctx->Iv, Iv, AES_BLOCKLEN); 511 | } 512 | 513 | void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, uint32_t length) 514 | { 515 | uintptr_t i; 516 | uint8_t storeNextIv[AES_BLOCKLEN]; 517 | for (i = 0; i < length; i += AES_BLOCKLEN) 518 | { 519 | memcpy(storeNextIv, buf, AES_BLOCKLEN); 520 | InvCipher((state_t*)buf, ctx->RoundKey); 521 | XorWithIv(buf, ctx->Iv); 522 | memcpy(ctx->Iv, storeNextIv, AES_BLOCKLEN); 523 | buf += AES_BLOCKLEN; 524 | } 525 | 526 | } 527 | 528 | #endif // #if defined(CBC) && (CBC == 1) 529 | 530 | 531 | 532 | #if defined(CTR) && (CTR == 1) 533 | 534 | /* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */ 535 | void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, uint32_t length) 536 | { 537 | uint8_t buffer[AES_BLOCKLEN]; 538 | 539 | unsigned i; 540 | int bi; 541 | for (i = 0, bi = AES_BLOCKLEN; i < length; ++i, ++bi) 542 | { 543 | if (bi == AES_BLOCKLEN) /* we need to regen xor compliment in buffer */ 544 | { 545 | 546 | memcpy(buffer, ctx->Iv, AES_BLOCKLEN); 547 | Cipher((state_t*)buffer,ctx->RoundKey); 548 | 549 | /* Increment Iv and handle overflow */ 550 | for (bi = (AES_BLOCKLEN - 1); bi >= 0; --bi) 551 | { 552 | /* inc will overflow */ 553 | if (ctx->Iv[bi] == 255) 554 | { 555 | ctx->Iv[bi] = 0; 556 | continue; 557 | } 558 | ctx->Iv[bi] += 1; 559 | break; 560 | } 561 | bi = 0; 562 | } 563 | 564 | buf[i] = (buf[i] ^ buffer[bi]); 565 | } 566 | } 567 | 568 | #endif // #if defined(CTR) && (CTR == 1) 569 | 570 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Ledger SAS 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![no_std] 16 | #![no_main] 17 | 18 | use nanos_sdk::buttons::ButtonEvent; 19 | use nanos_sdk::ecc; 20 | use nanos_sdk::io; 21 | use nanos_sdk::io::ApduHeader; 22 | use nanos_sdk::io::{Reply, StatusWords}; 23 | use nanos_sdk::nvm; 24 | use nanos_sdk::random; 25 | use nanos_sdk::NVMData; 26 | use nanos_ui::bagls; 27 | use nanos_ui::layout::Draw; 28 | use nanos_ui::ui; 29 | mod password; 30 | use heapless::Vec; 31 | use password::{ArrayString, PasswordItem}; 32 | mod tinyaes; 33 | use core::convert::TryFrom; 34 | use core::mem::MaybeUninit; 35 | 36 | nanos_sdk::set_panic!(nanos_sdk::exiting_panic); 37 | 38 | /// Stores all passwords in Non-Volatile Memory 39 | #[link_section = ".nvm_data"] 40 | static mut PASSWORDS: NVMData> = 41 | NVMData::new(nvm::Collection::new(PasswordItem::new())); 42 | 43 | /// Possible characters for the randomly generated passwords 44 | static PASS_CHARS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 45 | 46 | /// SLIP16 path for password encryption (used during export/import) 47 | static BIP32_PATH: [u32; 2] = ecc::make_bip32_path(b"m/10016'/0"); 48 | 49 | /// App Version parameters 50 | const NAME: &str = env!("CARGO_PKG_NAME"); 51 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 52 | 53 | enum Error { 54 | NoConsent, 55 | StorageFull, 56 | EntryNotFound, 57 | DecryptFailed, 58 | } 59 | 60 | impl Into for Error { 61 | fn into(self) -> Reply { 62 | match self { 63 | Error::NoConsent => Reply(0x69f0_u16), 64 | Error::StorageFull => Reply(0x9210_u16), 65 | Error::EntryNotFound => Reply(0x6a88_u16), 66 | Error::DecryptFailed => Reply(0x9d60_u16), 67 | } 68 | } 69 | } 70 | 71 | enum Instruction { 72 | GetVersion, 73 | GetSize, 74 | Add, 75 | GetName, 76 | GetByName, 77 | DeleteByName, 78 | Export, 79 | ExportNext, 80 | Import, 81 | ImportNext, 82 | Clear, 83 | Quit, 84 | ShowOnScreen, 85 | HasName, 86 | } 87 | 88 | impl TryFrom for Instruction { 89 | type Error = (); 90 | 91 | fn try_from(v: ApduHeader) -> Result { 92 | match v.ins { 93 | 0x01 => Ok(Self::GetVersion), 94 | 0x02 => Ok(Self::GetSize), 95 | 0x03 => Ok(Self::Add), 96 | 0x04 => Ok(Self::GetName), 97 | 0x05 => Ok(Self::GetByName), 98 | 0x06 => Ok(Self::DeleteByName), 99 | 0x07 => Ok(Self::Export), 100 | 0x08 => Ok(Self::ExportNext), 101 | 0x09 => Ok(Self::Import), 102 | 0x0a => Ok(Self::ImportNext), 103 | 0x0b => Ok(Self::Clear), 104 | 0x0c => Ok(Self::Quit), 105 | 0x0d => Ok(Self::ShowOnScreen), 106 | 0x0e => Ok(Self::HasName), 107 | _ => Err(()), 108 | } 109 | } 110 | } 111 | 112 | /// Basic Galois LFSR computation 113 | /// based on the wikipedia example... 114 | struct Lfsr { 115 | x: u8, 116 | m: u8, 117 | } 118 | 119 | impl Lfsr { 120 | pub fn new(init_val: u8, modulus: u8) -> Lfsr { 121 | if init_val == 0 { 122 | return Lfsr { x: 1, m: modulus }; 123 | } 124 | Lfsr { 125 | x: init_val, 126 | m: modulus, 127 | } 128 | } 129 | pub fn next(&mut self) -> u8 { 130 | let lsb = self.x & 1; 131 | self.x >>= 1; 132 | if lsb == 1 { 133 | self.x ^= self.m; 134 | } 135 | self.x 136 | } 137 | } 138 | 139 | #[no_mangle] 140 | extern "C" fn sample_main() { 141 | let mut comm = io::Comm::new(); 142 | 143 | // Don't use PASSWORDS directly in the program. It is static and using 144 | // it requires using unsafe everytime. Instead, take a reference here, so 145 | // in the rest of the program the borrow checker will be able to detect 146 | // misuses correctly. 147 | let mut passwords = unsafe { PASSWORDS.get_mut() }; 148 | 149 | // Encryption/decryption key for import and export. 150 | let mut enc_key = [0u8; 32]; 151 | let _ = ecc::bip32_derive(ecc::CurvesId::Secp256k1, &BIP32_PATH, &mut enc_key); 152 | 153 | // iteration counter 154 | let mut c = 0; 155 | // lfsr with period 16*4 - 1 (63), all pixels divided in 8 boxes 156 | let mut lfsr = Lfsr::new(u8::random() & 0x3f, 0x30); 157 | loop { 158 | match comm.next_event() { 159 | io::Event::Button(ButtonEvent::BothButtonsRelease) => nanos_sdk::exit_app(0), 160 | io::Event::Button(ButtonEvent::RightButtonRelease) => { 161 | display_infos(passwords); 162 | c = 0; 163 | } 164 | io::Event::Ticker => { 165 | let y_offset = ((nanos_ui::SCREEN_HEIGHT as i32) / 2) - 16; 166 | if c == 0 { 167 | bagls::RectFull::new() 168 | .pos(0, y_offset) 169 | .width(8) 170 | .height(8) 171 | .erase(); 172 | ui::SingleMessage::new("NanoPass").show(); 173 | lfsr = Lfsr::new(u8::random() & 0x3f, 0x30); 174 | } else if c == 128 { 175 | bagls::RectFull::new() 176 | .pos(1, y_offset + 1) 177 | .width(7) 178 | .height(7) 179 | .display(); 180 | } else if c >= 64 { 181 | let pos = lfsr.next(); 182 | let (x, y) = ((pos & 15) * 8, (pos >> 4) * 8); 183 | bagls::RectFull::new() 184 | .pos(x.into(), (y_offset + y as i32).into()) 185 | .width(8) 186 | .height(8) 187 | .erase(); 188 | let rect = bagls::RectFull::new() 189 | .pos((x + 1).into(), (y_offset + y as i32 + 1).into()) 190 | .width(7) 191 | .height(7); 192 | if c > 128 { 193 | rect.erase(); 194 | } else { 195 | rect.display(); 196 | } 197 | } 198 | c = (c + 1) % 192; 199 | } 200 | io::Event::Button(_) => {} 201 | // Get version string 202 | // Should comply with other apps standard 203 | io::Event::Command(Instruction::GetVersion) => { 204 | comm.append(&[1]); // Format 205 | comm.append(&[NAME.len() as u8]); 206 | comm.append(NAME.as_bytes()); 207 | comm.append(&[VERSION.len() as u8]); 208 | comm.append(VERSION.as_bytes()); 209 | comm.append(&[0]); // No flags 210 | comm.reply_ok(); 211 | } 212 | // Get number of stored passwords 213 | io::Event::Command(Instruction::GetSize) => { 214 | let len: [u8; 4] = passwords.len().to_be_bytes(); 215 | comm.append(&len); 216 | comm.reply_ok(); 217 | } 218 | // Add a password 219 | // If P1 == 0, password is in the data 220 | // If P1 == 1, password must be generated by the device 221 | io::Event::Command(Instruction::Add) => { 222 | let mut offset = 5; 223 | let name = ArrayString::<32>::from_bytes(comm.get(offset, offset + 32)); 224 | offset += 32; 225 | let login = ArrayString::<32>::from_bytes(comm.get(offset, offset + 32)); 226 | offset += 32; 227 | let pass = match comm.get_apdu_metadata().p1 { 228 | 0 => Some(ArrayString::<32>::from_bytes(comm.get(offset, offset + 32))), 229 | _ => None, 230 | }; 231 | comm.reply::(match set_password(passwords, &name, &login, &pass) { 232 | Ok(()) => StatusWords::Ok.into(), 233 | Err(e) => e.into(), 234 | }); 235 | c = 0; 236 | } 237 | // Get password name 238 | // This is used by the client to list the names of stored password 239 | // Login is not returned. 240 | io::Event::Command(Instruction::GetName) => { 241 | let mut index_bytes = [0; 4]; 242 | index_bytes.copy_from_slice(comm.get(5, 5 + 4)); 243 | let index = u32::from_be_bytes(index_bytes); 244 | match passwords.get(index as usize) { 245 | Some(password) => { 246 | comm.append(password.name.bytes()); 247 | comm.reply_ok() 248 | } 249 | None => comm.reply(Error::EntryNotFound), 250 | } 251 | } 252 | // Get password by name 253 | // Returns login and password data. 254 | io::Event::Command(Instruction::GetByName) => { 255 | let name = ArrayString::<32>::from_bytes(comm.get(5, 5 + 32)); 256 | 257 | match passwords.into_iter().find(|&&x| x.name == name) { 258 | Some(&p) => { 259 | if ui::MessageValidator::new( 260 | &[name.as_str()], 261 | &[&"Read", &"password"], 262 | &[&"Cancel"], 263 | ) 264 | .ask() 265 | { 266 | comm.append(p.login.bytes()); 267 | comm.append(p.pass.bytes()); 268 | comm.reply_ok(); 269 | } else { 270 | comm.reply(Error::NoConsent); 271 | } 272 | } 273 | None => { 274 | // Password not found 275 | comm.reply(Error::EntryNotFound); 276 | } 277 | } 278 | c = 0; 279 | } 280 | 281 | // Display a password on the screen only, without communicating it 282 | // to the host. 283 | io::Event::Command(Instruction::ShowOnScreen) => { 284 | let name = ArrayString::<32>::from_bytes(comm.get(5, 5 + 32)); 285 | 286 | match passwords.into_iter().find(|&&x| x.name == name) { 287 | Some(&p) => { 288 | if ui::MessageValidator::new( 289 | &[name.as_str()], 290 | &[&"Read", &"password"], 291 | &[&"Cancel"], 292 | ) 293 | .ask() 294 | { 295 | ui::popup(p.login.as_str()); 296 | ui::popup(p.pass.as_str()); 297 | comm.reply_ok(); 298 | } else { 299 | ui::popup("Operation cancelled"); 300 | comm.reply(Error::NoConsent); 301 | } 302 | } 303 | None => { 304 | ui::popup("Password not found"); 305 | comm.reply(Error::EntryNotFound); 306 | } 307 | } 308 | c = 0; 309 | } 310 | 311 | // Delete password by name 312 | io::Event::Command(Instruction::DeleteByName) => { 313 | let name = ArrayString::<32>::from_bytes(comm.get(5, 5 + 32)); 314 | match passwords.into_iter().position(|x| x.name == name) { 315 | Some(p) => { 316 | if ui::MessageValidator::new( 317 | &[name.as_str()], 318 | &[&"Remove", &"password"], 319 | &[&"Cancel"], 320 | ) 321 | .ask() 322 | { 323 | passwords.remove(p); 324 | comm.reply_ok(); 325 | } else { 326 | comm.reply(Error::NoConsent); 327 | } 328 | } 329 | None => { 330 | // Password not found 331 | comm.reply(Error::EntryNotFound); 332 | } 333 | } 334 | c = 0; 335 | } 336 | // Export 337 | // P1 can be 0 for plaintext, 1 for encrypted export. 338 | io::Event::Command(Instruction::Export) => match comm.get_apdu_metadata().p1 { 339 | 0 => export(&mut comm, &passwords, None), 340 | 1 => export(&mut comm, &passwords, Some(&enc_key)), 341 | _ => comm.reply(StatusWords::Unknown), 342 | }, 343 | // Reserved for export 344 | io::Event::Command(Instruction::ExportNext) => { 345 | comm.reply(StatusWords::Unknown); 346 | } 347 | // Import 348 | // P1 can be 0 for plaintext, 1 for encrypted import. 349 | io::Event::Command(Instruction::Import) => match comm.get_apdu_metadata().p1 { 350 | 0 => import(&mut comm, &mut passwords, None), 351 | 1 => import(&mut comm, &mut passwords, Some(&enc_key)), 352 | _ => comm.reply(StatusWords::Unknown), 353 | }, 354 | // Reserved for import 355 | io::Event::Command(Instruction::ImportNext) => { 356 | comm.reply(StatusWords::Unknown); 357 | } 358 | io::Event::Command(Instruction::Clear) => { 359 | // Remove all passwords 360 | comm.reply::( 361 | if ui::MessageValidator::new(&[], &[&"Remove all", &"passwords"], &[&"Cancel"]) 362 | .ask() 363 | { 364 | if ui::MessageValidator::new(&[], &[&"Are you", &"sure?"], &[&"Cancel"]) 365 | .ask() 366 | { 367 | passwords.clear(); 368 | StatusWords::Ok.into() 369 | } else { 370 | Error::NoConsent.into() 371 | } 372 | } else { 373 | Error::NoConsent.into() 374 | }, 375 | ); 376 | c = 0; 377 | } 378 | // Exit 379 | io::Event::Command(Instruction::Quit) => { 380 | comm.reply_ok(); 381 | nanos_sdk::exit_app(0); 382 | } 383 | // HasName 384 | io::Event::Command(Instruction::HasName) => { 385 | let name = ArrayString::<32>::from_bytes(comm.get(5, 5 + 32)); 386 | match passwords.into_iter().find(|&&x| x.name == name) { 387 | Some(_) => { 388 | comm.append(&[1]); 389 | } 390 | None => { 391 | comm.append(&[0]); 392 | } 393 | } 394 | comm.reply_ok(); 395 | } 396 | } 397 | } 398 | } 399 | 400 | /// Conversion to a two-digit number 401 | fn int2dec(x: usize) -> [u8; 2] { 402 | let mut t = (x % 100) as u16; 403 | if t == 0 { 404 | return [b' ', b'0']; 405 | } 406 | let mut dec = [b' '; 2]; 407 | dec[1] = b'0' + (t as u8) % 10; 408 | t /= 10; 409 | if t != 0 { 410 | dec[0] = b'0' + (t as u8) % 10; 411 | } 412 | dec 413 | } 414 | 415 | /// Display global information about the app: 416 | /// - Current number of passwords stored 417 | /// - App Version 418 | fn display_infos(passwords: &nvm::Collection) { 419 | let mut stored_n = *b" passwords"; 420 | let pwlen_bytes = int2dec(passwords.len()); 421 | 422 | stored_n[0] = pwlen_bytes[0]; 423 | stored_n[1] = pwlen_bytes[1]; 424 | 425 | // safety: int2dec returns a [u8; 2] consisting of values between 426 | // '0' and '9', thus is valid utf8 427 | let stored_str = unsafe { core::str::from_utf8_unchecked(&stored_n) }; 428 | 429 | const APP_VERSION_STR: &str = concat!(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); 430 | 431 | ui::Menu::new(&[APP_VERSION_STR, stored_str]).show(); 432 | } 433 | 434 | /// Generates a random password. 435 | /// 436 | /// # Arguments 437 | /// 438 | /// * `dest` - An array where the result is stored. Must be at least 439 | /// `size` long. No terminal zero is written. 440 | /// * `size` - The size of the password to be generated 441 | use random::Random; 442 | fn generate_random_password(dest: &mut [u8], size: usize) { 443 | for item in dest.iter_mut().take(size) { 444 | let rand_index = u32::random_from_range(0..PASS_CHARS.len() as u32); 445 | *item = PASS_CHARS.chars().nth(rand_index as usize).unwrap() as u8; 446 | } 447 | } 448 | 449 | /// Adds or update a password in the store. 450 | /// Queries confirmation from the user in the UX. 451 | /// 452 | /// # Arguments 453 | /// 454 | /// * `name` - Slice to the new name of the password. Must be 32 bytes long. 455 | /// * `login` - Slice to the new login of the password. Must be 32 bytes long. 456 | /// * `pass` - New password. If None, a password is generated automatically. 457 | fn set_password( 458 | passwords: &mut nvm::Collection, 459 | name: &ArrayString<32>, 460 | login: &ArrayString<32>, 461 | pass: &Option>, 462 | ) -> Result<(), Error> { 463 | // Create the item to be added. 464 | let mut new_item = PasswordItem::new(); 465 | new_item.name = *name; 466 | new_item.login = *login; 467 | match pass { 468 | Some(a) => new_item.pass = *a, 469 | None => { 470 | let mut pass = [0u8; 16]; 471 | let len = pass.len(); 472 | generate_random_password(&mut pass, len); 473 | new_item.pass.set_from_bytes(&pass); 474 | } 475 | } 476 | 477 | return match passwords.into_iter().position(|x| x.name == *name) { 478 | Some(index) => { 479 | // A password with this name already exists. 480 | if !ui::MessageValidator::new(&[name.as_str()], &[&"Update", &"password"], &[&"Cancel"]) 481 | .ask() 482 | { 483 | return Err(Error::NoConsent); 484 | } 485 | passwords.remove(index); 486 | match passwords.add(&new_item) { 487 | Ok(()) => Ok(()), 488 | // We just removed a password, this should not happen 489 | Err(nvm::StorageFullError) => panic!(), 490 | } 491 | } 492 | None => { 493 | // Ask user confirmation 494 | if !ui::MessageValidator::new(&[name.as_str()], &[&"Create", &"password"], &[&"Cancel"]) 495 | .ask() 496 | { 497 | return Err(Error::NoConsent); 498 | } 499 | match passwords.add(&new_item) { 500 | Ok(()) => Ok(()), 501 | Err(nvm::StorageFullError) => Err(Error::StorageFull), 502 | } 503 | } 504 | }; 505 | } 506 | 507 | /// Export procedure. 508 | /// 509 | /// # Arguments 510 | /// 511 | /// * `enc_key` - Encryption key. If None, passwords are exported in plaintext. 512 | fn export( 513 | comm: &mut io::Comm, 514 | passwords: &nvm::Collection, 515 | enc_key: Option<&[u8; 32]>, 516 | ) { 517 | // Ask user confirmation 518 | if !ui::MessageValidator::new(&[], &[&"Export", &"passwords"], &[&"Cancel"]).ask() { 519 | comm.reply(Error::NoConsent); 520 | return; 521 | } 522 | 523 | // If export is in plaintext, add a warning 524 | let encrypted = enc_key.is_some(); 525 | if !encrypted 526 | && !ui::MessageValidator::new(&[&"Export is plaintext!"], &[&"Confirm"], &[&"Cancel"]).ask() 527 | { 528 | comm.reply(Error::NoConsent); 529 | return; 530 | } 531 | 532 | // User accepted. Reply with the number of passwords 533 | let count = passwords.len(); 534 | comm.append(&count.to_be_bytes()); 535 | comm.reply_ok(); 536 | 537 | // We are now waiting for N APDUs to retrieve all passwords. 538 | // If encryption is enabled, the IV is returned during the first iteration. 539 | ui::SingleMessage::new("Exporting...").show(); 540 | 541 | let mut iter = passwords.into_iter(); 542 | let mut next_item = iter.next(); 543 | while next_item.is_some() { 544 | match comm.next_command() { 545 | // Fetch next password 546 | Instruction::ExportNext => { 547 | let password = next_item.unwrap(); 548 | // If encryption is enabled, encrypt the buffer inplace. 549 | if encrypted { 550 | let mut nonce = [0u8; 16]; 551 | random::rand_bytes(&mut nonce); 552 | comm.append(&nonce); 553 | let mut buffer: Vec = Vec::new(); 554 | buffer.extend_from_slice(password.name.bytes()).unwrap(); 555 | buffer.extend_from_slice(password.login.bytes()).unwrap(); 556 | buffer.extend_from_slice(password.pass.bytes()).unwrap(); 557 | // Encrypt buffer in AES-256-CBC with random IV 558 | let mut aes_ctx = MaybeUninit::::uninit(); 559 | unsafe { 560 | tinyaes::AES_init_ctx_iv( 561 | aes_ctx.as_mut_ptr(), 562 | enc_key.unwrap().as_ptr(), 563 | nonce.as_ptr(), 564 | ); 565 | tinyaes::AES_CBC_encrypt_buffer( 566 | aes_ctx.as_mut_ptr(), 567 | buffer.as_mut_ptr(), 568 | buffer.len() as u32, 569 | ); 570 | } 571 | comm.append(&buffer as &[u8]); 572 | // Now calculate AES-256-CBC-MAC 573 | unsafe { 574 | tinyaes::AES_init_ctx_iv( 575 | aes_ctx.as_mut_ptr(), 576 | enc_key.unwrap().as_ptr(), 577 | nonce.as_ptr(), 578 | ); 579 | tinyaes::AES_CBC_encrypt_buffer( 580 | aes_ctx.as_mut_ptr(), 581 | buffer.as_mut_ptr(), 582 | buffer.len() as u32, 583 | ); 584 | } 585 | let mac = &buffer[buffer.len() - 16..]; 586 | comm.append(mac); 587 | } else { 588 | comm.append(password.name.bytes()); 589 | comm.append(password.login.bytes()); 590 | comm.append(password.pass.bytes()); 591 | } 592 | comm.reply_ok(); 593 | // Advance iterator. 594 | next_item = iter.next(); 595 | } 596 | _ => { 597 | comm.reply(StatusWords::Unknown); 598 | return; 599 | } 600 | } 601 | } 602 | } 603 | 604 | /// Import procedure. 605 | /// 606 | /// # Arguments 607 | /// 608 | /// * `enc_key` - Encryption key. If None, passwords are imported as plaintext. 609 | fn import( 610 | comm: &mut io::Comm, 611 | passwords: &mut nvm::Collection, 612 | enc_key: Option<&[u8; 32]>, 613 | ) { 614 | let encrypted = enc_key.is_some(); 615 | 616 | // Retrieve the number of passwords to be imported 617 | let mut count_bytes = [0u8; 4]; 618 | count_bytes.copy_from_slice(comm.get(5, 5 + 4)); 619 | let mut count = u32::from_be_bytes(count_bytes); 620 | // Ask user confirmation 621 | if !ui::MessageValidator::new(&[], &[&"Import", &"passwords"], &[&"Cancel"]).ask() { 622 | comm.reply(Error::NoConsent); 623 | return; 624 | } else { 625 | comm.reply_ok(); 626 | } 627 | // Wait for all items 628 | ui::SingleMessage::new("Importing...").show(); 629 | while count > 0 { 630 | match comm.next_command() { 631 | // Fetch next password 632 | Instruction::ImportNext => { 633 | count -= 1; 634 | let mut new_item = PasswordItem::new(); 635 | let mut decrypt_failed = false; 636 | if encrypted { 637 | let nonce = comm.get(5, 5 + 16); 638 | let mut buffer: Vec = Vec::new(); 639 | buffer 640 | .extend_from_slice(comm.get(5 + 16, 5 + 16 + 96)) 641 | .unwrap(); 642 | // Decrypt with AES-256-CBC 643 | let mut aes_ctx = MaybeUninit::::uninit(); 644 | unsafe { 645 | tinyaes::AES_init_ctx_iv( 646 | aes_ctx.as_mut_ptr(), 647 | enc_key.unwrap().as_ptr(), 648 | nonce.as_ptr(), 649 | ); 650 | tinyaes::AES_CBC_decrypt_buffer( 651 | aes_ctx.as_mut_ptr(), 652 | buffer.as_mut_ptr(), 653 | buffer.len() as u32, 654 | ); 655 | } 656 | new_item.name = ArrayString::<32>::from_bytes(&buffer[..32]); 657 | new_item.login = ArrayString::<32>::from_bytes(&buffer[32..64]); 658 | new_item.pass = ArrayString::<32>::from_bytes(&buffer[64..96]); 659 | // Verify the MAC 660 | buffer.clear(); 661 | buffer 662 | .extend_from_slice(comm.get(5 + 16, 5 + 16 + 96)) 663 | .unwrap(); 664 | unsafe { 665 | tinyaes::AES_init_ctx_iv( 666 | aes_ctx.as_mut_ptr(), 667 | enc_key.unwrap().as_ptr(), 668 | nonce.as_ptr(), 669 | ); 670 | tinyaes::AES_CBC_encrypt_buffer( 671 | aes_ctx.as_mut_ptr(), 672 | buffer.as_mut_ptr(), 673 | buffer.len() as u32, 674 | ); 675 | } 676 | let received_mac = comm.get(5 + 16 + 96, 5 + 16 + 96 + 16); 677 | let expected_mac = &buffer[buffer.len() - 16..]; 678 | decrypt_failed = received_mac != expected_mac; 679 | } else { 680 | let mut offset = 5; 681 | new_item.name = ArrayString::<32>::from_bytes(comm.get(offset, offset + 32)); 682 | offset += 32; 683 | new_item.login = ArrayString::<32>::from_bytes(comm.get(offset, offset + 32)); 684 | offset += 32; 685 | new_item.pass = ArrayString::<32>::from_bytes(comm.get(offset, offset + 32)); 686 | } 687 | if !decrypt_failed { 688 | if let Some(index) = passwords.into_iter().position(|x| x.name == new_item.name) 689 | { 690 | passwords.remove(index); 691 | } 692 | comm.reply::(match passwords.add(&new_item) { 693 | Ok(()) => StatusWords::Ok.into(), 694 | Err(nvm::StorageFullError) => Error::StorageFull.into(), 695 | }); 696 | } else { 697 | comm.reply(Error::DecryptFailed); 698 | break; 699 | } 700 | } 701 | _ => { 702 | comm.reply(StatusWords::BadCla); 703 | break; 704 | } 705 | } 706 | } 707 | } 708 | --------------------------------------------------------------------------------