From 4200af146a17398dc7050c92e1f861f2066debec Mon Sep 17 00:00:00 2001 From: Amit Aronovitch Date: Wed, 2 Oct 2019 00:01:48 +0300 Subject: Identify Librem Key, behaving like Nitrokey Pro device --- NK_C_API.cc | 8 ++++++ NK_C_API.h | 11 ++++++-- NitrokeyManager.cc | 27 ++++++++++++++++--- device.cc | 75 ++++++++++++++++++++++++++++++++++++++-------------- libnitrokey/device.h | 19 +++++++++++-- 5 files changed, 113 insertions(+), 27 deletions(-) diff --git a/NK_C_API.cc b/NK_C_API.cc index 1d3fa3a..75c8b97 100644 --- a/NK_C_API.cc +++ b/NK_C_API.cc @@ -158,6 +158,9 @@ extern "C" { case NK_STORAGE: model_string = "S"; break; + case NK_LIBREM: + model_string = "L"; + break; case NK_DISCONNECTED: default: /* no such enum value -- return error code */ @@ -236,6 +239,8 @@ extern "C" { return NK_PRO; case DeviceModel::STORAGE: return NK_STORAGE; + case DeviceModel::LIBREM: + return NK_LIBREM; default: /* unknown or not connected device */ return NK_device_model::NK_DISCONNECTED; @@ -791,6 +796,9 @@ NK_C_API char* NK_get_SD_usage_data_as_string() { case DeviceModel::STORAGE: target->model = NK_STORAGE; break; + case DeviceModel::LIBREM: + target->model = NK_LIBREM; + break; default: return false; } diff --git a/NK_C_API.h b/NK_C_API.h index d5c54a3..5b93f08 100644 --- a/NK_C_API.h +++ b/NK_C_API.h @@ -67,6 +67,9 @@ * case NK_STORAGE: * printf("a Nitrokey Storage"); * break; + * case NK_LIBREM: + * printf("a Librem Key"); + * break; * default: * printf("an unsupported Nitrokey"); * break; @@ -111,7 +114,11 @@ extern "C" { /** * Nitrokey Storage. */ - NK_STORAGE = 2 + NK_STORAGE = 2, + /** + * Librem Key. + */ + NK_LIBREM = 3 }; /** @@ -327,7 +334,7 @@ extern "C" { /** * Connect to device of given model. Currently library can be connected only to one device at once. - * @param device_model NK_device_model: NK_PRO: Nitrokey Pro, NK_STORAGE: Nitrokey Storage + * @param device_model NK_device_model: NK_PRO: Nitrokey Pro, NK_STORAGE: Nitrokey Storage, NK_LIBREM: Librem Key * @return 1 if connected, 0 if wrong model or cannot connect */ NK_C_API int NK_login_enum(enum NK_device_model device_model); diff --git a/NitrokeyManager.cc b/NitrokeyManager.cc index 6c26a43..d874dca 100644 --- a/NitrokeyManager.cc +++ b/NitrokeyManager.cc @@ -217,7 +217,12 @@ using nitrokey::misc::strcpyT; } } - auto info_ptr = hid_enumerate(NITROKEY_VID, 0); + auto vendor_id = NITROKEY_VID; + auto info_ptr = hid_enumerate(vendor_id, 0); + if (!info_ptr) { + vendor_id = PURISM_VID; + info_ptr = hid_enumerate(vendor_id, 0); + } auto first_info_ptr = info_ptr; if (!info_ptr) return false; @@ -225,7 +230,7 @@ using nitrokey::misc::strcpyT; misc::Option model; while (info_ptr && !model.has_value()) { if (path == std::string(info_ptr->path)) { - model = product_id_to_model(info_ptr->product_id); + model = product_id_to_model(vendor_id, info_ptr->product_id); } info_ptr = info_ptr->next; } @@ -254,7 +259,8 @@ using nitrokey::misc::strcpyT; bool NitrokeyManager::connect() { std::lock_guard lock(mex_dev_com_manager); - vector< shared_ptr > devices = { make_shared(), make_shared() }; + vector< shared_ptr > devices = { make_shared(), make_shared(), + make_shared() }; bool connected = false; for( auto & d : devices ){ if (d->connect()){ @@ -290,6 +296,9 @@ using nitrokey::misc::strcpyT; case 'S': device = make_shared(); break; + case 'L': + device = make_shared(); + break; default: throw std::runtime_error("Unknown model"); } @@ -305,6 +314,9 @@ using nitrokey::misc::strcpyT; case device::DeviceModel::STORAGE: model_string = "S"; break; + case device::DeviceModel::LIBREM: + model_string = "L"; + break; default: throw std::runtime_error("Unknown model"); } @@ -382,6 +394,7 @@ using nitrokey::misc::strcpyT; string NitrokeyManager::get_serial_number() { if (device == nullptr) { return ""; }; switch (device->get_device_model()) { + case DeviceModel::LIBREM: case DeviceModel::PRO: { auto response = GetStatus::CommandTransaction::run(device); return nitrokey::misc::toHex(response.data().card_serial_u32); @@ -552,6 +565,7 @@ using nitrokey::misc::strcpyT; strcpyT(payload.slot_name, slot_name); strcpyT(payload.slot_token_id, token_ID); switch (device->get_device_model() ){ + case DeviceModel::LIBREM: case DeviceModel::PRO: { payload.slot_counter = hotp_counter; break; @@ -713,6 +727,7 @@ using nitrokey::misc::strcpyT; template void NitrokeyManager::change_PIN_general(const char *current_PIN, const char *new_PIN) { switch (device->get_device_model()){ + case DeviceModel::LIBREM: case DeviceModel::PRO: { auto p = get_payload(); @@ -834,6 +849,7 @@ using nitrokey::misc::strcpyT; void NitrokeyManager::build_aes_key(const char *admin_password) { switch (device->get_device_model()) { + case DeviceModel::LIBREM: case DeviceModel::PRO: { auto p = get_payload(); strcpyT(p.admin_password, admin_password); @@ -858,6 +874,7 @@ using nitrokey::misc::strcpyT; void NitrokeyManager::unlock_user_password(const char *admin_password, const char *new_user_password) { switch (device->get_device_model()){ + case DeviceModel::LIBREM: case DeviceModel::PRO: { auto p = get_payload(); strcpyT(p.admin_password, admin_password); @@ -907,6 +924,7 @@ using nitrokey::misc::strcpyT; //authorization command is supported for versions equal or below: auto m = std::unordered_map({ {DeviceModel::PRO, 7}, + {DeviceModel::LIBREM, 7}, {DeviceModel::STORAGE, 53}, }); return get_minor_firmware_version() <= m[device->get_device_model()]; @@ -916,6 +934,7 @@ using nitrokey::misc::strcpyT; // 320 bit OTP secret is supported by version bigger or equal to: auto m = std::unordered_map({ {DeviceModel::PRO, 8}, + {DeviceModel::LIBREM, 8}, {DeviceModel::STORAGE, 54}, }); return get_minor_firmware_version() >= m[device->get_device_model()]; @@ -940,6 +959,7 @@ using nitrokey::misc::strcpyT; uint8_t NitrokeyManager::get_minor_firmware_version(){ switch(device->get_device_model()){ + case DeviceModel::LIBREM: case DeviceModel::PRO:{ auto status_p = GetStatus::CommandTransaction::run(device); return status_p.data().firmware_version_st.minor; //7 or 8 @@ -956,6 +976,7 @@ using nitrokey::misc::strcpyT; } uint8_t NitrokeyManager::get_major_firmware_version(){ switch(device->get_device_model()){ + case DeviceModel::LIBREM: case DeviceModel::PRO:{ auto status_p = GetStatus::CommandTransaction::run(device); return status_p.data().firmware_version_st.major; //0 diff --git a/device.cc b/device.cc index bc42965..5f89f2c 100644 --- a/device.cc +++ b/device.cc @@ -45,14 +45,29 @@ const uint16_t nitrokey::device::NITROKEY_VID = 0x20a0; const uint16_t nitrokey::device::NITROKEY_PRO_PID = 0x4108; const uint16_t nitrokey::device::NITROKEY_STORAGE_PID = 0x4109; -Option nitrokey::device::product_id_to_model(uint16_t product_id) { - switch (product_id) { +const uint16_t nitrokey::device::PURISM_VID = 0x316d; +const uint16_t nitrokey::device::LIBREM_KEY_PID = 0x4c4b; + +Option nitrokey::device::product_id_to_model(uint16_t vendor_id, uint16_t product_id) { + switch (vendor_id) { + case NITROKEY_VID: + switch (product_id) { case NITROKEY_PRO_PID: return DeviceModel::PRO; case NITROKEY_STORAGE_PID: return DeviceModel::STORAGE; default: return {}; + } + case PURISM_VID: + switch (product_id) { + case LIBREM_KEY_PID: + return DeviceModel::LIBREM; + default: + return {}; + } + default: + return {}; } } @@ -67,6 +82,9 @@ std::ostream& nitrokey::device::operator<<(std::ostream& stream, DeviceModel mod case DeviceModel::STORAGE: stream << "Storage"; break; + case DeviceModel::LIBREM: + stream << "Librem"; + break; default: stream << "Unknown"; break; @@ -99,7 +117,9 @@ bool Device::disconnect() { } bool Device::_disconnect() { - LOG(std::string(__FUNCTION__) + std::string(m_model == DeviceModel::PRO ? "PRO" : "STORAGE"), Loglevel::DEBUG_L2); + LOG(std::string(__FUNCTION__) + + std::string(m_model == DeviceModel::PRO ? "PRO" : (m_model == DeviceModel::STORAGE ? "STORAGE" : "LIBREM")), + Loglevel::DEBUG_L2); LOG(std::string(__FUNCTION__) + std::string(" *IN* "), Loglevel::DEBUG_L2); if(mp_devhandle == nullptr) { @@ -204,27 +224,33 @@ int Device::recv(void *packet) { return status; } -std::vector Device::enumerate(){ - auto pInfo = hid_enumerate(NITROKEY_VID, 0); - auto pInfo_ = pInfo; - std::vector res; - while (pInfo != nullptr){ - auto deviceModel = product_id_to_model(pInfo->product_id); - if (deviceModel.has_value()) { - std::string path(pInfo->path); - std::wstring serialNumberW(pInfo->serial_number); - std::wstring_convert> converter; - std::string serialNumber = converter.to_bytes(serialNumberW); - DeviceInfo info = { deviceModel.value(), path, serialNumber }; - res.push_back(info); +namespace { + void add_vendor_devices(std::vector& res, uint16_t vendor_id){ + auto pInfo = hid_enumerate(vendor_id, 0); + auto pInfo_ = pInfo; + while (pInfo != nullptr){ + auto deviceModel = product_id_to_model(vendor_id, pInfo->product_id); + if (deviceModel.has_value()) { + std::string path(pInfo->path); + std::wstring serialNumberW(pInfo->serial_number); + std::wstring_convert> converter; + std::string serialNumber = converter.to_bytes(serialNumberW); + DeviceInfo info = { deviceModel.value(), path, serialNumber }; + res.push_back(info); + } + pInfo = pInfo->next; } - pInfo = pInfo->next; - } - if (pInfo_ != nullptr){ - hid_free_enumeration(pInfo_); + if (pInfo_ != nullptr){ + hid_free_enumeration(pInfo_); + } } +} +std::vector Device::enumerate(){ + std::vector res; + ::add_vendor_devices(res, NITROKEY_VID); + ::add_vendor_devices(res, PURISM_VID); return res; } @@ -234,6 +260,8 @@ std::shared_ptr Device::create(DeviceModel model) { return std::make_shared(); case DeviceModel::STORAGE: return std::make_shared(); + case DeviceModel::LIBREM: + return std::make_shared(); default: return {}; } @@ -305,6 +333,13 @@ Stick20::Stick20(): setDefaultDelay(); } + +LibremKey::LibremKey(): + Device(PURISM_VID, LIBREM_KEY_PID, DeviceModel::LIBREM, 100ms, 5, 100ms) + { + setDefaultDelay(); + } + #include #define p(x) ss << #x << " " << x << ", "; std::string Device::ErrorCounters::get_as_string() { diff --git a/libnitrokey/device.h b/libnitrokey/device.h index d50080d..d39310a 100644 --- a/libnitrokey/device.h +++ b/libnitrokey/device.h @@ -50,7 +50,8 @@ namespace device { enum class DeviceModel{ PRO, - STORAGE + STORAGE, + LIBREM }; std::ostream& operator<<(std::ostream& stream, DeviceModel model); @@ -67,12 +68,20 @@ extern const uint16_t NITROKEY_PRO_PID; * The USB product ID for the Nitrokey Storage. */ extern const uint16_t NITROKEY_STORAGE_PID; +/** + * The USB vendor ID for Purism devices. + */ +extern const uint16_t PURISM_VID; +/** + * The USB product ID for the Librem Key. + */ +extern const uint16_t LIBREM_KEY_PID; /** * Convert the given USB product ID to a Nitrokey model. If there is no model * with that ID, return an absent value. */ -misc::Option product_id_to_model(uint16_t product_id); +misc::Option product_id_to_model(uint16_t vendor_id, uint16_t product_id); /** * Information about a connected device. @@ -219,6 +228,12 @@ class Stick20 : public Device { public: Stick20(); }; + +class LibremKey : public Device { + public: + LibremKey(); +}; + } } #endif -- cgit v1.2.1 From 2f1c76bf82619e0809a41a1df4fe0e26c6315270 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 6 Jan 2020 13:16:00 +0100 Subject: device: Check for nullptr in Device::enumerate While the fields of the hid_device_info struct should not be null, it is better to perform an explicit check before trying to construct strings from them. --- device.cc | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/device.cc b/device.cc index bc42965..5f26d65 100644 --- a/device.cc +++ b/device.cc @@ -209,14 +209,16 @@ std::vector Device::enumerate(){ auto pInfo_ = pInfo; std::vector res; while (pInfo != nullptr){ - auto deviceModel = product_id_to_model(pInfo->product_id); - if (deviceModel.has_value()) { - std::string path(pInfo->path); - std::wstring serialNumberW(pInfo->serial_number); - std::wstring_convert> converter; - std::string serialNumber = converter.to_bytes(serialNumberW); - DeviceInfo info = { deviceModel.value(), path, serialNumber }; - res.push_back(info); + if (pInfo->path != nullptr && pInfo->serial_number != nullptr) { + auto deviceModel = product_id_to_model(pInfo->product_id); + if (deviceModel.has_value()) { + std::string path(pInfo->path); + std::wstring serialNumberW(pInfo->serial_number); + std::wstring_convert> converter; + std::string serialNumber = converter.to_bytes(serialNumberW); + DeviceInfo info = { deviceModel.value(), path, serialNumber }; + res.push_back(info); + } } pInfo = pInfo->next; } -- cgit v1.2.1 From 9e1f590d5a929a21223bb39ae5123d27e69fda66 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Fri, 17 Jan 2020 18:19:45 +0100 Subject: Add tests for Pro`s bootloader --- unittest/constants.py | 3 +++ unittest/test_pro.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/unittest/constants.py b/unittest/constants.py index 645ef6a..bf4f5b9 100644 --- a/unittest/constants.py +++ b/unittest/constants.py @@ -39,6 +39,9 @@ class DefaultPasswords: USER_TEMP = b'234234234' UPDATE = b'12345678' UPDATE_TEMP = b'123update123' + UPDATE_LONG = b'1234567890'*2 + UPDATE_TOO_LONG = UPDATE_LONG + b'x' + UPDATE_TOO_SHORT = b'1234567' class DeviceErrorCode: diff --git a/unittest/test_pro.py b/unittest/test_pro.py index a8df7cd..3f1cb8d 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -977,21 +977,48 @@ def test_get_device_model(C): # assert C.NK_get_device_model() != C.NK_DISCONNECTED +@pytest.mark.firmware +def test_bootloader_password_change_pro_length(C): + skip_if_device_version_lower_than({'P': 11}) + + # Test whether the correct password is set + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + # Change to the longest possible password + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_LONG) == DeviceErrorCode.STATUS_OK + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE_LONG, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + # Use longer or shorter passwords than possible + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TOO_LONG) == LibraryErrors.TOO_LONG_STRING + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TOO_SHORT) == DeviceErrorCode.WRONG_PASSWORD + + + @pytest.mark.firmware def test_bootloader_password_change_pro(C): skip_if_device_version_lower_than({'P': 11}) assert C.NK_change_firmware_password_pro(b'zxcasd', b'zxcasd') == DeviceErrorCode.WRONG_PASSWORD + # Revert effects of broken test run, if needed + C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE_TEMP, DefaultPasswords.UPDATE) + + # Change to the same password + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + # Change password assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.STATUS_OK assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE_TEMP, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK @pytest.mark.firmware -def test_bootloader_run_pro(C): +def test_bootloader_run_pro_wrong_password(C): skip_if_device_version_lower_than({'P': 11}) assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.WRONG_PASSWORD + + +@pytest.mark.skip +@pytest.mark.firmware +def test_bootloader_run_pro(C): # Not enabled due to lack of side-effect removal at this point - # assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK @pytest.mark.firmware -- cgit v1.2.1 From 4d6a01cd2f2ec6440f5dd2c02048cbb413f91f5d Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Mon, 27 Jan 2020 18:18:55 +0100 Subject: Correct result of successful execution for bootloader to DISCONNECTED --- unittest/constants.py | 1 + unittest/test_pro.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/unittest/constants.py b/unittest/constants.py index bf4f5b9..9e04701 100644 --- a/unittest/constants.py +++ b/unittest/constants.py @@ -52,6 +52,7 @@ class DeviceErrorCode: STATUS_NOT_AUTHORIZED = 5 STATUS_AES_DEC_FAILED = 0xa STATUS_UNKNOWN_ERROR = 100 + STATUS_DISCONNECTED = 255 class LibraryErrors: diff --git a/unittest/test_pro.py b/unittest/test_pro.py index 3f1cb8d..d26aa7c 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -1018,7 +1018,7 @@ def test_bootloader_run_pro_wrong_password(C): @pytest.mark.firmware def test_bootloader_run_pro(C): # Not enabled due to lack of side-effect removal at this point - assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_DISCONNECTED @pytest.mark.firmware -- cgit v1.2.1 From fdce2aee7dacbbfaad300fd6bef6dd70c0766604 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Mon, 27 Jan 2020 18:19:24 +0100 Subject: Change too short password to default, but short of 1 letter --- unittest/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unittest/constants.py b/unittest/constants.py index 9e04701..1ba52fa 100644 --- a/unittest/constants.py +++ b/unittest/constants.py @@ -41,7 +41,7 @@ class DefaultPasswords: UPDATE_TEMP = b'123update123' UPDATE_LONG = b'1234567890'*2 UPDATE_TOO_LONG = UPDATE_LONG + b'x' - UPDATE_TOO_SHORT = b'1234567' + UPDATE_TOO_SHORT = UPDATE_LONG[:7] class DeviceErrorCode: -- cgit v1.2.1 From 73251ccf5abae5ae91f2aa962a29dce6fcc0e5e4 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Mon, 27 Jan 2020 19:14:09 +0100 Subject: Rename the actual bootloader switching test to a distinct name --- unittest/test_pro.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unittest/test_pro.py b/unittest/test_pro.py index d26aa7c..3a324bb 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -1016,7 +1016,7 @@ def test_bootloader_run_pro_wrong_password(C): @pytest.mark.skip @pytest.mark.firmware -def test_bootloader_run_pro(C): +def test_bootloader_run_pro_real(C): # Not enabled due to lack of side-effect removal at this point assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_DISCONNECTED -- cgit v1.2.1 From ec767410d860688f32ef644e9573841272d5aec2 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Tue, 28 Jan 2020 14:45:34 +0100 Subject: Skip bootloader activation unless specified by the switch --- unittest/conftest.py | 8 ++++++++ unittest/test_pro.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/unittest/conftest.py b/unittest/conftest.py index 49b4f02..1377e50 100644 --- a/unittest/conftest.py +++ b/unittest/conftest.py @@ -155,3 +155,11 @@ def get_library(request, allow_offline=False): return AttrProxy(C, "libnitrokey C") + +def pytest_addoption(parser): + parser.addoption("--run-skipped", action="store_true", + help="run the tests skipped by default, e.g. adding side effects") + +def pytest_runtest_setup(item): + if 'skip_by_default' in item.keywords and not item.config.getoption("--run-skipped"): + pytest.skip("need --run-skipped option to run this test") \ No newline at end of file diff --git a/unittest/test_pro.py b/unittest/test_pro.py index 3a324bb..a47d804 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -1014,7 +1014,7 @@ def test_bootloader_run_pro_wrong_password(C): assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.WRONG_PASSWORD -@pytest.mark.skip +@pytest.mark.skip_by_default @pytest.mark.firmware def test_bootloader_run_pro_real(C): # Not enabled due to lack of side-effect removal at this point -- cgit v1.2.1 From 2b37ae486deba1985179543333c592b55fafbc37 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Tue, 28 Jan 2020 14:46:04 +0100 Subject: Bootloader data retention test --- unittest/test_pro.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/unittest/test_pro.py b/unittest/test_pro.py index a47d804..4ac19ab 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -1029,6 +1029,23 @@ def test_bootloader_password_change_pro_too_long(C): assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, long_string) == LibraryErrors.TOO_LONG_STRING +@pytest.mark.skip_by_default +@pytest.mark.firmware +def test_bootloader_data_rention_test(C): + skip_if_device_version_lower_than({'P': 11}) + + def populate_device(): + return False + + def check_data_on_device(): + return False + + assert populate_device() + assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_DISCONNECTED + input('Please press ENTER after uploading new firmware to the device') + assert check_data_on_device() + + @pytest.mark.otp @pytest.mark.parametrize('counter_mid', [10**3-1, 10**4-1, 10**7-1, 10**8-10, 2**16, 2**31-1, 2**32-1, 2**33, 2**50, 2**60, 2**63]) # 2**64-1 def test_HOTP_counter_getter(C, counter_mid: int): -- cgit v1.2.1 From 0f0225c63b6770c62ca13fcd31fd0e3e83674d93 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Tue, 28 Jan 2020 14:49:26 +0100 Subject: Refactor rename --- unittest/test_pro.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/unittest/test_pro.py b/unittest/test_pro.py index 4ac19ab..2a5f02a 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -88,21 +88,21 @@ def test_write_all_password_safe_slots_and_read_10_times(C): @pytest.mark.slowtest @pytest.mark.xfail(reason="This test should be run directly after test_write_all_password_safe_slots_and_read_10_times") def test_read_all_password_safe_slots_10_times(C): - def fill(s, wid): - assert wid >= len(s) + def helper_fill(str_to_fill, target_width): + assert target_width >= len(str_to_fill) numbers = '1234567890'*4 - s += numbers[:wid-len(s)] - assert len(s) == wid - return bb(s) + str_to_fill += numbers[:target_width - len(str_to_fill)] + assert len(str_to_fill) == target_width + return bb(str_to_fill) - def get_pass(suffix): - return fill('pass' + suffix, 20) + def helper_PWS_get_pass(suffix): + return helper_fill('pass' + suffix, 20) - def get_loginname(suffix): - return fill('login' + suffix, 32) + def helper_PWS_get_loginname(suffix): + return helper_fill('login' + suffix, 32) - def get_slotname(suffix): - return fill('slotname' + suffix, 11) + def helper_PWS_get_slotname(suffix): + return helper_fill('slotname' + suffix, 11) assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK @@ -111,9 +111,9 @@ def test_read_all_password_safe_slots_10_times(C): for j in range(0, 10): for i in range(0, PWS_slot_count): iss = str(i) - assert gs(C.NK_get_password_safe_slot_name(i)) == get_slotname(iss) - assert gs(C.NK_get_password_safe_slot_login(i)) == get_loginname(iss) - assert gs(C.NK_get_password_safe_slot_password(i)) == get_pass(iss) + assert gs(C.NK_get_password_safe_slot_name(i)) == helper_PWS_get_slotname(iss) + assert gs(C.NK_get_password_safe_slot_login(i)) == helper_PWS_get_loginname(iss) + assert gs(C.NK_get_password_safe_slot_password(i)) == helper_PWS_get_pass(iss) @pytest.mark.lock_device -- cgit v1.2.1 From 59ca679eeb272163e4ebbb2f01231adc4820cda1 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Tue, 28 Jan 2020 14:52:09 +0100 Subject: Refactor move --- unittest/constants.py | 2 -- unittest/helpers.py | 22 ++++++++++++++++++++++ unittest/test_pro.py | 19 ++----------------- 3 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 unittest/helpers.py diff --git a/unittest/constants.py b/unittest/constants.py index 1ba52fa..b73dfe8 100644 --- a/unittest/constants.py +++ b/unittest/constants.py @@ -21,8 +21,6 @@ SPDX-License-Identifier: LGPL-3.0 from misc import to_hex -def bb(x): - return bytes(x, encoding='ascii') RFC_SECRET_HR = '12345678901234567890' diff --git a/unittest/helpers.py b/unittest/helpers.py new file mode 100644 index 0000000..79f4e1e --- /dev/null +++ b/unittest/helpers.py @@ -0,0 +1,22 @@ +def bb(x): + return bytes(x, encoding='ascii') + + +def helper_fill(str_to_fill, target_width): + assert target_width >= len(str_to_fill) + numbers = '1234567890' * 4 + str_to_fill += numbers[:target_width - len(str_to_fill)] + assert len(str_to_fill) == target_width + return bb(str_to_fill) + + +def helper_PWS_get_pass(suffix): + return helper_fill('pass' + suffix, 20) + + +def helper_PWS_get_loginname(suffix): + return helper_fill('login' + suffix, 32) + + +def helper_PWS_get_slotname(suffix): + return helper_fill('slotname' + suffix, 11) \ No newline at end of file diff --git a/unittest/test_pro.py b/unittest/test_pro.py index 2a5f02a..233d4d2 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -22,8 +22,9 @@ SPDX-License-Identifier: LGPL-3.0 import pytest from conftest import skip_if_device_version_lower_than -from constants import DefaultPasswords, DeviceErrorCode, RFC_SECRET, bb, bbRFC_SECRET, LibraryErrors, HOTP_slot_count, \ +from constants import DefaultPasswords, DeviceErrorCode, RFC_SECRET, bbRFC_SECRET, LibraryErrors, HOTP_slot_count, \ TOTP_slot_count +from helpers import helper_PWS_get_slotname, helper_PWS_get_loginname, helper_PWS_get_pass, bb from misc import ffi, gs, wait, cast_pointer_to_tuple, has_binary_counter from misc import is_storage @@ -88,22 +89,6 @@ def test_write_all_password_safe_slots_and_read_10_times(C): @pytest.mark.slowtest @pytest.mark.xfail(reason="This test should be run directly after test_write_all_password_safe_slots_and_read_10_times") def test_read_all_password_safe_slots_10_times(C): - def helper_fill(str_to_fill, target_width): - assert target_width >= len(str_to_fill) - numbers = '1234567890'*4 - str_to_fill += numbers[:target_width - len(str_to_fill)] - assert len(str_to_fill) == target_width - return bb(str_to_fill) - - def helper_PWS_get_pass(suffix): - return helper_fill('pass' + suffix, 20) - - def helper_PWS_get_loginname(suffix): - return helper_fill('login' + suffix, 32) - - def helper_PWS_get_slotname(suffix): - return helper_fill('slotname' + suffix, 11) - assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK PWS_slot_count = 16 -- cgit v1.2.1 From 2fa7e29b725085e62dd647da6e0dd28bcfe91e43 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Tue, 28 Jan 2020 14:53:41 +0100 Subject: Refactor move 2 --- unittest/test_pro.py | 69 -------------------------------------- unittest/test_pro_bootloader.py | 73 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 69 deletions(-) create mode 100644 unittest/test_pro_bootloader.py diff --git a/unittest/test_pro.py b/unittest/test_pro.py index 233d4d2..13efd28 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -962,75 +962,6 @@ def test_get_device_model(C): # assert C.NK_get_device_model() != C.NK_DISCONNECTED -@pytest.mark.firmware -def test_bootloader_password_change_pro_length(C): - skip_if_device_version_lower_than({'P': 11}) - - # Test whether the correct password is set - assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK - # Change to the longest possible password - assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_LONG) == DeviceErrorCode.STATUS_OK - assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE_LONG, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK - # Use longer or shorter passwords than possible - assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TOO_LONG) == LibraryErrors.TOO_LONG_STRING - assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TOO_SHORT) == DeviceErrorCode.WRONG_PASSWORD - - - -@pytest.mark.firmware -def test_bootloader_password_change_pro(C): - skip_if_device_version_lower_than({'P': 11}) - assert C.NK_change_firmware_password_pro(b'zxcasd', b'zxcasd') == DeviceErrorCode.WRONG_PASSWORD - - # Revert effects of broken test run, if needed - C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE_TEMP, DefaultPasswords.UPDATE) - - # Change to the same password - assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK - assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK - # Change password - assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.STATUS_OK - assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE_TEMP, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK - - -@pytest.mark.firmware -def test_bootloader_run_pro_wrong_password(C): - skip_if_device_version_lower_than({'P': 11}) - assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.WRONG_PASSWORD - - -@pytest.mark.skip_by_default -@pytest.mark.firmware -def test_bootloader_run_pro_real(C): - # Not enabled due to lack of side-effect removal at this point - assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_DISCONNECTED - - -@pytest.mark.firmware -def test_bootloader_password_change_pro_too_long(C): - skip_if_device_version_lower_than({'P': 11}) - long_string = b'a' * 100 - assert C.NK_change_firmware_password_pro(long_string, long_string) == LibraryErrors.TOO_LONG_STRING - assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, long_string) == LibraryErrors.TOO_LONG_STRING - - -@pytest.mark.skip_by_default -@pytest.mark.firmware -def test_bootloader_data_rention_test(C): - skip_if_device_version_lower_than({'P': 11}) - - def populate_device(): - return False - - def check_data_on_device(): - return False - - assert populate_device() - assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_DISCONNECTED - input('Please press ENTER after uploading new firmware to the device') - assert check_data_on_device() - - @pytest.mark.otp @pytest.mark.parametrize('counter_mid', [10**3-1, 10**4-1, 10**7-1, 10**8-10, 2**16, 2**31-1, 2**32-1, 2**33, 2**50, 2**60, 2**63]) # 2**64-1 def test_HOTP_counter_getter(C, counter_mid: int): diff --git a/unittest/test_pro_bootloader.py b/unittest/test_pro_bootloader.py new file mode 100644 index 0000000..4a6b272 --- /dev/null +++ b/unittest/test_pro_bootloader.py @@ -0,0 +1,73 @@ +import pytest + +from conftest import skip_if_device_version_lower_than +from constants import DefaultPasswords, DeviceErrorCode, LibraryErrors + + +@pytest.mark.firmware +def test_bootloader_password_change_pro_length(C): + skip_if_device_version_lower_than({'P': 11}) + + # Test whether the correct password is set + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + # Change to the longest possible password + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_LONG) == DeviceErrorCode.STATUS_OK + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE_LONG, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + # Use longer or shorter passwords than possible + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TOO_LONG) == LibraryErrors.TOO_LONG_STRING + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TOO_SHORT) == DeviceErrorCode.WRONG_PASSWORD + + + +@pytest.mark.firmware +def test_bootloader_password_change_pro(C): + skip_if_device_version_lower_than({'P': 11}) + assert C.NK_change_firmware_password_pro(b'zxcasd', b'zxcasd') == DeviceErrorCode.WRONG_PASSWORD + + # Revert effects of broken test run, if needed + C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE_TEMP, DefaultPasswords.UPDATE) + + # Change to the same password + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + # Change password + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE_TEMP, DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_OK + + +@pytest.mark.firmware +def test_bootloader_run_pro_wrong_password(C): + skip_if_device_version_lower_than({'P': 11}) + assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.WRONG_PASSWORD + + +@pytest.mark.skip_by_default +@pytest.mark.firmware +def test_bootloader_run_pro_real(C): + # Not enabled due to lack of side-effect removal at this point + assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_DISCONNECTED + + +@pytest.mark.firmware +def test_bootloader_password_change_pro_too_long(C): + skip_if_device_version_lower_than({'P': 11}) + long_string = b'a' * 100 + assert C.NK_change_firmware_password_pro(long_string, long_string) == LibraryErrors.TOO_LONG_STRING + assert C.NK_change_firmware_password_pro(DefaultPasswords.UPDATE, long_string) == LibraryErrors.TOO_LONG_STRING + + +@pytest.mark.skip_by_default +@pytest.mark.firmware +def test_bootloader_data_rention_test(C): + skip_if_device_version_lower_than({'P': 11}) + + def populate_device(): + return False + + def check_data_on_device(): + return False + + assert populate_device() + assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_DISCONNECTED + input('Please press ENTER after uploading new firmware to the device') + assert check_data_on_device() -- cgit v1.2.1 From b0550556c02625e59c11212421c4b81d9cddc961 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Tue, 25 Feb 2020 14:08:34 +0100 Subject: Refactor. Allow device reconnection. --- unittest/conftest.py | 98 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/unittest/conftest.py b/unittest/conftest.py index 1377e50..17d9ef5 100644 --- a/unittest/conftest.py +++ b/unittest/conftest.py @@ -20,6 +20,7 @@ SPDX-License-Identifier: LGPL-3.0 """ import pytest +import os, sys from misc import ffi, gs @@ -82,47 +83,8 @@ def C(request=None): def get_library(request, allow_offline=False): - fp = '../NK_C_API.h' - - declarations = [] - with open(fp, 'r') as f: - declarations = f.readlines() - - cnt = 0 - a = iter(declarations) - for declaration in a: - if declaration.strip().startswith('NK_C_API') \ - or declaration.strip().startswith('struct'): - declaration = declaration.replace('NK_C_API', '').strip() - while ');' not in declaration and '};' not in declaration: - declaration += (next(a)).strip()+'\n' - ffi.cdef(declaration, override=True) - cnt += 1 - print('Imported {} declarations'.format(cnt)) - - C = None - import os, sys - path_build = os.path.join("..", "build") - paths = [ - os.environ.get('LIBNK_PATH', None), - os.path.join(path_build,"libnitrokey.so"), - os.path.join(path_build,"libnitrokey.dylib"), - os.path.join(path_build,"libnitrokey.dll"), - os.path.join(path_build,"nitrokey.dll"), - ] - for p in paths: - if not p: continue - print("Trying " +p) - p = os.path.abspath(p) - if os.path.exists(p): - print("Found: "+p) - C = ffi.dlopen(p) - break - else: - print("File does not exist: " + p) - if not C: - print("No library file found") - sys.exit(1) + library_read_declarations() + C = library_open_lib() C.NK_set_debug_level(int(os.environ.get('LIBNK_DEBUG', 2))) @@ -156,10 +118,62 @@ def get_library(request, allow_offline=False): return AttrProxy(C, "libnitrokey C") +def library_open_lib(): + C = None + path_build = os.path.join("..", "build") + paths = [ + os.environ.get('LIBNK_PATH', None), + os.path.join(path_build, "libnitrokey.so"), + os.path.join(path_build, "libnitrokey.dylib"), + os.path.join(path_build, "libnitrokey.dll"), + os.path.join(path_build, "nitrokey.dll"), + ] + for p in paths: + if not p: continue + print("Trying " + p) + p = os.path.abspath(p) + if os.path.exists(p): + print("Found: " + p) + C = ffi.dlopen(p) + break + else: + print("File does not exist: " + p) + if not C: + print("No library file found") + sys.exit(1) + return C + + +def library_read_declarations(): + fp = '../NK_C_API.h' + declarations = [] + with open(fp, 'r') as f: + declarations = f.readlines() + cnt = 0 + a = iter(declarations) + for declaration in a: + if declaration.strip().startswith('NK_C_API') \ + or declaration.strip().startswith('struct'): + declaration = declaration.replace('NK_C_API', '').strip() + while ');' not in declaration and '};' not in declaration: + declaration += (next(a)).strip() + '\n' + ffi.cdef(declaration, override=True) + cnt += 1 + print('Imported {} declarations'.format(cnt)) + + def pytest_addoption(parser): parser.addoption("--run-skipped", action="store_true", help="run the tests skipped by default, e.g. adding side effects") def pytest_runtest_setup(item): if 'skip_by_default' in item.keywords and not item.config.getoption("--run-skipped"): - pytest.skip("need --run-skipped option to run this test") \ No newline at end of file + pytest.skip("need --run-skipped option to run this test") + + +def library_device_reconnect(C): + C.NK_logout() + C = library_open_lib() + C.NK_logout() + assert C.NK_login_auto() == 1, 'Device not found' + return C \ No newline at end of file -- cgit v1.2.1 From 55f9b7293c22bbedd5a972fa8f1946dfd57d9c7a Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Tue, 25 Feb 2020 14:10:30 +0100 Subject: Data retention test. Refactoring. Helper functions. --- unittest/constants.py | 8 +++----- unittest/helpers.py | 35 ++++++++++++++++++++++++++++++++--- unittest/misc.py | 4 ++++ unittest/test_multiple.py | 4 ++-- unittest/test_pro.py | 30 +++++++----------------------- unittest/test_pro_bootloader.py | 17 +++++++---------- unittest/test_storage.py | 4 ++-- 7 files changed, 57 insertions(+), 45 deletions(-) diff --git a/unittest/constants.py b/unittest/constants.py index b73dfe8..4047f59 100644 --- a/unittest/constants.py +++ b/unittest/constants.py @@ -18,10 +18,7 @@ along with libnitrokey. If not, see . SPDX-License-Identifier: LGPL-3.0 """ - -from misc import to_hex - - +from misc import to_hex, bb RFC_SECRET_HR = '12345678901234567890' RFC_SECRET = to_hex(RFC_SECRET_HR) # '31323334353637383930...' @@ -61,4 +58,5 @@ class LibraryErrors: HOTP_slot_count = 3 -TOTP_slot_count = 15 \ No newline at end of file +TOTP_slot_count = 15 +PWS_SLOT_COUNT = 16 diff --git a/unittest/helpers.py b/unittest/helpers.py index 79f4e1e..90c818e 100644 --- a/unittest/helpers.py +++ b/unittest/helpers.py @@ -1,5 +1,5 @@ -def bb(x): - return bytes(x, encoding='ascii') +from constants import DeviceErrorCode, PWS_SLOT_COUNT, DefaultPasswords +from misc import gs, bb def helper_fill(str_to_fill, target_width): @@ -19,4 +19,33 @@ def helper_PWS_get_loginname(suffix): def helper_PWS_get_slotname(suffix): - return helper_fill('slotname' + suffix, 11) \ No newline at end of file + return helper_fill('slotname' + suffix, 11) + + +def helper_check_device_for_data(C): + assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK + assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK + + for i in range(0, PWS_SLOT_COUNT): + iss = str(i) + assert gs(C.NK_get_password_safe_slot_name(i)) == helper_PWS_get_slotname(iss) + assert gs(C.NK_get_password_safe_slot_login(i)) == helper_PWS_get_loginname(iss) + assert gs(C.NK_get_password_safe_slot_password(i)) == helper_PWS_get_pass(iss) + return True + + +def helper_populate_device(C): + # FIXME use object with random data, and check against it + # FIXME generate OTP as well, and check codes against its secrets + assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK + res = C.NK_enable_password_safe(DefaultPasswords.USER) + if res != DeviceErrorCode.STATUS_OK: + assert C.NK_build_aes_key(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK + assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK + + for i in range(0, PWS_SLOT_COUNT): + iss = str(i) + assert C.NK_write_password_safe_slot(i, + helper_PWS_get_slotname(iss), helper_PWS_get_loginname(iss), + helper_PWS_get_pass(iss)) == DeviceErrorCode.STATUS_OK + return True diff --git a/unittest/misc.py b/unittest/misc.py index e9e1753..6a0d486 100644 --- a/unittest/misc.py +++ b/unittest/misc.py @@ -72,3 +72,7 @@ def is_long_OTP_secret_handled(C): def has_binary_counter(C): return (not is_storage(C)) or (is_storage(C) and get_devices_firmware_version(C) >= 54) + + +def bb(x): + return bytes(x, encoding='ascii') \ No newline at end of file diff --git a/unittest/test_multiple.py b/unittest/test_multiple.py index 821a3b7..96b23d7 100644 --- a/unittest/test_multiple.py +++ b/unittest/test_multiple.py @@ -28,8 +28,8 @@ from collections import defaultdict from tqdm import tqdm from conftest import skip_if_device_version_lower_than -from constants import DefaultPasswords, DeviceErrorCode, bb -from misc import gs, wait, ffi +from constants import DefaultPasswords, DeviceErrorCode +from misc import gs, wait, ffi, bb pprint = pprint.PrettyPrinter(indent=4).pprint diff --git a/unittest/test_pro.py b/unittest/test_pro.py index 13efd28..99d7b1f 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -24,8 +24,8 @@ import pytest from conftest import skip_if_device_version_lower_than from constants import DefaultPasswords, DeviceErrorCode, RFC_SECRET, bbRFC_SECRET, LibraryErrors, HOTP_slot_count, \ TOTP_slot_count -from helpers import helper_PWS_get_slotname, helper_PWS_get_loginname, helper_PWS_get_pass, bb -from misc import ffi, gs, wait, cast_pointer_to_tuple, has_binary_counter +from helpers import helper_PWS_get_slotname, helper_PWS_get_loginname, helper_PWS_get_pass +from misc import ffi, gs, wait, cast_pointer_to_tuple, has_binary_counter, bb from misc import is_storage @pytest.mark.lock_device @@ -51,37 +51,21 @@ def test_write_password_safe_slot(C): @pytest.mark.PWS @pytest.mark.slowtest def test_write_all_password_safe_slots_and_read_10_times(C): - def fill(s, wid): - assert wid >= len(s) - numbers = '1234567890'*4 - s += numbers[:wid-len(s)] - assert len(s) == wid - return bb(s) - - def get_pass(suffix): - return fill('pass' + suffix, 20) - - def get_loginname(suffix): - return fill('login' + suffix, 32) - - def get_slotname(suffix): - return fill('slotname' + suffix, 11) - assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK PWS_slot_count = 16 for i in range(0, PWS_slot_count): iss = str(i) assert C.NK_write_password_safe_slot(i, - get_slotname(iss), get_loginname(iss), - get_pass(iss)) == DeviceErrorCode.STATUS_OK + helper_PWS_get_slotname(iss), helper_PWS_get_loginname(iss), + helper_PWS_get_pass(iss)) == DeviceErrorCode.STATUS_OK for j in range(0, 10): for i in range(0, PWS_slot_count): iss = str(i) - assert gs(C.NK_get_password_safe_slot_name(i)) == get_slotname(iss) - assert gs(C.NK_get_password_safe_slot_login(i)) == get_loginname(iss) - assert gs(C.NK_get_password_safe_slot_password(i)) == get_pass(iss) + assert gs(C.NK_get_password_safe_slot_name(i)) == helper_PWS_get_slotname(iss) + assert gs(C.NK_get_password_safe_slot_login(i)) == helper_PWS_get_loginname(iss) + assert gs(C.NK_get_password_safe_slot_password(i)) == helper_PWS_get_pass(iss) @pytest.mark.lock_device diff --git a/unittest/test_pro_bootloader.py b/unittest/test_pro_bootloader.py index 4a6b272..b33a9d7 100644 --- a/unittest/test_pro_bootloader.py +++ b/unittest/test_pro_bootloader.py @@ -1,7 +1,8 @@ import pytest -from conftest import skip_if_device_version_lower_than +from conftest import skip_if_device_version_lower_than, library_device_reconnect from constants import DefaultPasswords, DeviceErrorCode, LibraryErrors +from helpers import helper_populate_device, helper_check_device_for_data @pytest.mark.firmware @@ -58,16 +59,12 @@ def test_bootloader_password_change_pro_too_long(C): @pytest.mark.skip_by_default @pytest.mark.firmware -def test_bootloader_data_rention_test(C): +def test_bootloader_data_rention(C): skip_if_device_version_lower_than({'P': 11}) - def populate_device(): - return False - - def check_data_on_device(): - return False - - assert populate_device() + assert helper_populate_device(C) assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_DISCONNECTED input('Please press ENTER after uploading new firmware to the device') - assert check_data_on_device() + C = library_device_reconnect(C) + assert helper_check_device_for_data(C) + diff --git a/unittest/test_storage.py b/unittest/test_storage.py index 0f960cc..a435a15 100644 --- a/unittest/test_storage.py +++ b/unittest/test_storage.py @@ -23,8 +23,8 @@ import pprint import pytest from conftest import skip_if_device_version_lower_than -from constants import DefaultPasswords, DeviceErrorCode, bb -from misc import gs, wait, ffi +from constants import DefaultPasswords, DeviceErrorCode +from misc import gs, wait, ffi, bb pprint = pprint.PrettyPrinter(indent=4).pprint -- cgit v1.2.1 From f37b771bbb8b665f4c0c1fe6f8336cf4fb458e5d Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Tue, 25 Feb 2020 14:11:58 +0100 Subject: Add missing skip for lower firmware version --- unittest/test_pro_bootloader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unittest/test_pro_bootloader.py b/unittest/test_pro_bootloader.py index b33a9d7..4cb7470 100644 --- a/unittest/test_pro_bootloader.py +++ b/unittest/test_pro_bootloader.py @@ -45,6 +45,7 @@ def test_bootloader_run_pro_wrong_password(C): @pytest.mark.skip_by_default @pytest.mark.firmware def test_bootloader_run_pro_real(C): + skip_if_device_version_lower_than({'P': 11}) # Not enabled due to lack of side-effect removal at this point assert C.NK_enable_firmware_update_pro(DefaultPasswords.UPDATE) == DeviceErrorCode.STATUS_DISCONNECTED -- cgit v1.2.1 From 67b14773cf4ab1812af85d3aaf99bdc6119c5a8a Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 2 Apr 2020 15:02:36 +0200 Subject: NitrokeyManager: Also return serial number as u32 This patch adds the get_serial_number_as_u32 method to NitrokeyManager. It returns the serial number as a 32-bit unsigned integer. Previously, we only returned it as a string generated from the integer value, get_serial_number. While get_serial_number returns an empty string if no device is connected and "NA" if an unknown model is connected, the new method throws a DeviceNotConnected exception in the first case and returns zero in the second case as we cannot express the three states in one integer return value. --- NitrokeyManager.cc | 19 +++++++++++++++++++ libnitrokey/NitrokeyManager.h | 1 + unittest/test_offline.cc | 4 ++++ 3 files changed, 24 insertions(+) diff --git a/NitrokeyManager.cc b/NitrokeyManager.cc index 6c26a43..496496e 100644 --- a/NitrokeyManager.cc +++ b/NitrokeyManager.cc @@ -398,6 +398,25 @@ using nitrokey::misc::strcpyT; return "NA"; } + uint32_t NitrokeyManager::get_serial_number_as_u32() { + if (device == nullptr) { throw DeviceNotConnected("device not connected"); } + switch (device->get_device_model()) { + case DeviceModel::PRO: { + auto response = GetStatus::CommandTransaction::run(device); + return response.data().card_serial_u32; + } + break; + + case DeviceModel::STORAGE: + { + auto response = stick20::GetDeviceStatus::CommandTransaction::run(device); + return response.data().ActiveSmartCardID_u32; + } + break; + } + return 0; + } + stick10::GetStatus::ResponsePayload NitrokeyManager::get_status(){ try{ auto response = GetStatus::CommandTransaction::run(device); diff --git a/libnitrokey/NitrokeyManager.h b/libnitrokey/NitrokeyManager.h index 33ede1b..163a799 100644 --- a/libnitrokey/NitrokeyManager.h +++ b/libnitrokey/NitrokeyManager.h @@ -104,6 +104,7 @@ char * strndup(const char* str, size_t maxlen); stick10::GetStatus::ResponsePayload get_status(); string get_status_as_string(); string get_serial_number(); + uint32_t get_serial_number_as_u32(); char * get_totp_slot_name(uint8_t slot_number); char * get_hotp_slot_name(uint8_t slot_number); diff --git a/unittest/test_offline.cc b/unittest/test_offline.cc index 320ad48..3ca3905 100644 --- a/unittest/test_offline.cc +++ b/unittest/test_offline.cc @@ -66,6 +66,10 @@ TEST_CASE("Test C++ side behaviour in offline", "[fast]") { REQUIRE_NOTHROW (serial_number = i->get_serial_number()); REQUIRE(serial_number.empty()); + REQUIRE_THROWS_AS( + i->get_serial_number_as_u32(), DeviceNotConnected + ); + REQUIRE_THROWS_AS( i->get_status(), DeviceNotConnected ); -- cgit v1.2.1 From 02f19713c155457e8cc7d80d581bdac35cb1716d Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 2 Apr 2020 15:14:11 +0200 Subject: NitrokeyManager: Refactor get_serial_number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To avoid duplicate code, this patch refactors the get_serial_number method to use get_serial_number_as_u32 internally. It does not change get_serial_number’s behavior. --- NitrokeyManager.cc | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/NitrokeyManager.cc b/NitrokeyManager.cc index 496496e..b6a076a 100644 --- a/NitrokeyManager.cc +++ b/NitrokeyManager.cc @@ -380,22 +380,16 @@ using nitrokey::misc::strcpyT; string NitrokeyManager::get_serial_number() { - if (device == nullptr) { return ""; }; - switch (device->get_device_model()) { - case DeviceModel::PRO: { - auto response = GetStatus::CommandTransaction::run(device); - return nitrokey::misc::toHex(response.data().card_serial_u32); - } - break; - - case DeviceModel::STORAGE: - { - auto response = stick20::GetDeviceStatus::CommandTransaction::run(device); - return nitrokey::misc::toHex(response.data().ActiveSmartCardID_u32); + try { + auto serial_number = this->get_serial_number_as_u32(); + if (serial_number == 0) { + return "NA"; + } else { + return nitrokey::misc::toHex(serial_number); } - break; + } catch (DeviceNotConnected& e) { + return ""; } - return "NA"; } uint32_t NitrokeyManager::get_serial_number_as_u32() { -- cgit v1.2.1 From fedf828e394938fb6f84407b4de7412a3fb6ec40 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 2 Apr 2020 15:19:00 +0200 Subject: Return serial number as uint32_t from C API This patch adds the function NK_device_serial_number_as_u32 to the C API. It is similar to NK_device_serial_number but returns the raw unsigned integer instead of a formatted string. This patch also adds a simple test case that ensures that the number is not zero. Fixes #172. --- NK_C_API.cc | 7 +++++++ NK_C_API.h | 8 ++++++++ unittest/test_pro.py | 9 ++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/NK_C_API.cc b/NK_C_API.cc index 1d3fa3a..c44e36f 100644 --- a/NK_C_API.cc +++ b/NK_C_API.cc @@ -299,6 +299,13 @@ extern "C" { }); } + NK_C_API uint32_t NK_device_serial_number_as_u32() { + auto m = NitrokeyManager::instance(); + return get_with_result([&]() { + return m->get_serial_number_as_u32(); + }); + } + NK_C_API char * NK_get_hotp_code(uint8_t slot_number) { return NK_get_hotp_code_PIN(slot_number, ""); } diff --git a/NK_C_API.h b/NK_C_API.h index d5c54a3..df3e992 100644 --- a/NK_C_API.h +++ b/NK_C_API.h @@ -385,6 +385,14 @@ extern "C" { */ NK_C_API char * NK_device_serial_number(); + /** + * Return the device's serial number string as an integer. Use + * NK_last_command_status to check for an error if this function + * returns zero. + * @return device's serial number as an integer + */ + NK_C_API uint32_t NK_device_serial_number_as_u32(); + /** * Get last command processing status. Useful for commands which returns the results of their own and could not return * an error code. diff --git a/unittest/test_pro.py b/unittest/test_pro.py index 99d7b1f..d25a50e 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -704,6 +704,13 @@ def test_get_serial_number(C): print(('Serial number of the device: ', sn)) +@pytest.mark.status +def test_get_serial_number_as_u32(C): + sn = C.NK_device_serial_number_as_u32() + assert sn > 0 + print(('Serial number of the device (u32): ', sn)) + + @pytest.mark.otp @pytest.mark.parametrize("secret", ['000001', '00'*10+'ff', '00'*19+'ff', '000102', '00'*29+'ff', '00'*39+'ff', '002EF43F51AFA97BA2B46418768123C9E1809A5B' ]) @@ -1038,4 +1045,4 @@ def test_OTP_all_rw(C): this_loop_codes.append(('H', i, code)) all_codes.append(this_loop_codes) from pprint import pprint - pprint(all_codes) \ No newline at end of file + pprint(all_codes) -- cgit v1.2.1 From b482abe3bf62ef46816e81ccdab68bc24f498ce6 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 2 Apr 2020 15:38:39 +0200 Subject: Add test_memory test case This patch adds a test_memory test case that demonstrates new[]/free mismatches with the NK_read_config and NK_get_password_safe_slot_status functions if run with valgrind, see #173. --- CMakeLists.txt | 1 + unittest/test_memory.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 unittest/test_memory.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 1169e94..db8d2fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -216,6 +216,7 @@ IF (COMPILE_TESTS) unittest/test_HOTP.cc unittest/test1.cc unittest/test_issues.cc + unittest/test_memory.c unittest/test_multiple_devices.cc unittest/test_strdup.cpp unittest/test_safe.cpp diff --git a/unittest/test_memory.c b/unittest/test_memory.c new file mode 100644 index 0000000..34ea7d5 --- /dev/null +++ b/unittest/test_memory.c @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2020 Nitrokey UG + * + * This file is part of libnitrokey. + * + * libnitrokey is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * libnitrokey is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnitrokey. If not, see . + * + * SPDX-License-Identifier: LGPL-3.0 + */ + +#include +#include "../NK_C_API.h" + +// This test should be run with valgrind to make sure that there are no +// memory leaks in the tested functions: +// valgrind ./test_memory +int main() { + int result = NK_login_auto(); + if (result != 1) + return 1; + + int retry_count = NK_get_admin_retry_count(); + if (retry_count != 3) + return 1; + retry_count = NK_get_user_retry_count(); + if (retry_count != 3) + return 1; + + enum NK_device_model model = NK_get_device_model(); + if (model != NK_PRO && model != NK_STORAGE) + return 1; + + uint8_t *config = NK_read_config(); + if (config == NULL) + return 1; + free(config); + + result = NK_enable_password_safe("123456"); + if (result != 0) + return 1; + + uint8_t *slot_status = NK_get_password_safe_slot_status(); + if (slot_status == NULL) { + return 1; + } + free(slot_status); + + NK_logout(); + + return 0; +} + -- cgit v1.2.1 From 2a7b3f4e2ae09d665f9783030323dfb1a4c5ee9f Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 2 Apr 2020 15:51:25 +0200 Subject: Add missing free functions to C API This patch adds two missing free functions, NK_free_config and NK_free_password_safe_slot_status, to enable memory-safe use of the C API. Fixes #173. --- NK_C_API.cc | 8 ++++++++ NK_C_API.h | 14 ++++++++++++++ unittest/test_memory.c | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/NK_C_API.cc b/NK_C_API.cc index 1d3fa3a..0b7f5f7 100644 --- a/NK_C_API.cc +++ b/NK_C_API.cc @@ -226,6 +226,10 @@ extern "C" { }); } + NK_C_API void NK_free_config(uint8_t* config) { + delete[] config; + } + NK_C_API enum NK_device_model NK_get_device_model() { auto m = NitrokeyManager::instance(); @@ -448,6 +452,10 @@ extern "C" { } + NK_C_API void NK_free_password_safe_slot_status(uint8_t* status) { + delete[] status; + } + NK_C_API uint8_t NK_get_user_retry_count() { auto m = NitrokeyManager::instance(); return get_with_result([&]() { diff --git a/NK_C_API.h b/NK_C_API.h index d5c54a3..c4cb448 100644 --- a/NK_C_API.h +++ b/NK_C_API.h @@ -451,6 +451,7 @@ extern "C" { /** * Get currently set config - status of function Numlock/Capslock/Scrollock OTP sending and is enabled PIN protected OTP + * The return value must be freed using NK_free_config. * @see NK_write_config * @return uint8_t general_config[5]: * uint8_t numlock; @@ -462,6 +463,12 @@ extern "C" { */ NK_C_API uint8_t* NK_read_config(); + /** + * Free a value returned by NK_read_config. May be called with a NULL + * argument. + */ + NK_C_API void NK_free_config(uint8_t* config); + //OTP /** @@ -634,10 +641,17 @@ extern "C" { /** * Get password safe slots' status + * The return value must be freed using NK_free_password_safe_slot_status. * @return uint8_t[16] slot statuses - each byte represents one slot with 0 (not programmed) and 1 (programmed) */ NK_C_API uint8_t * NK_get_password_safe_slot_status(); + /** + * Free a value returned by NK_get_password_safe_slot_status. May be + * called with a NULL argument. + */ + NK_C_API void NK_free_password_safe_slot_status(uint8_t* status); + /** * Get password safe slot name * @param slot_number password safe slot number, slot_number<16 diff --git a/unittest/test_memory.c b/unittest/test_memory.c index 34ea7d5..20b11b2 100644 --- a/unittest/test_memory.c +++ b/unittest/test_memory.c @@ -44,7 +44,7 @@ int main() { uint8_t *config = NK_read_config(); if (config == NULL) return 1; - free(config); + NK_free_config(config); result = NK_enable_password_safe("123456"); if (result != 0) @@ -54,7 +54,7 @@ int main() { if (slot_status == NULL) { return 1; } - free(slot_status); + NK_free_password_safe_slot_status(slot_status); NK_logout(); -- cgit v1.2.1 From 0270a9b3de4b45fcfcb83f8e20a78702811d4192 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Thu, 2 Apr 2020 16:29:27 +0200 Subject: Add NK_config struct and read/write functions This patch adds the NK_config struct to the C API that stores the general configuration of a Nitrokey device. It also adds the NK_read_config_struct and NK_write_config_struct functions to make the API easier to use. While NK_write_config_struct is only a convenience method, NK_read_config_struct makes the API more safe as the user no longer has to read the data from a pointer to an array. This patch also extends the test_read_write_config test case with the two new functions. --- NK_C_API.cc | 21 +++++++++++++++++++++ NK_C_API.h | 43 +++++++++++++++++++++++++++++++++++++++++++ unittest/test_pro.py | 26 +++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/NK_C_API.cc b/NK_C_API.cc index 1d3fa3a..d993671 100644 --- a/NK_C_API.cc +++ b/NK_C_API.cc @@ -217,6 +217,12 @@ extern "C" { }); } + NK_C_API int NK_write_config_struct(struct NK_config config, + const char *admin_temporary_password) { + return NK_write_config(config.numlock, config.capslock, config.scrolllock, config.enable_user_password, + config.disable_user_password, admin_temporary_password); + } + NK_C_API uint8_t* NK_read_config() { auto m = NitrokeyManager::instance(); @@ -226,6 +232,21 @@ extern "C" { }); } + NK_C_API int NK_read_config_struct(struct NK_config* out) { + if (out == nullptr) { + return -1; + } + auto m = NitrokeyManager::instance(); + return get_without_result([&]() { + auto v = m->read_config(); + out->numlock = v[0]; + out->capslock = v[1]; + out->scrolllock = v[2]; + out->enable_user_password = v[3]; + out->disable_user_password = v[4]; + }); + } + NK_C_API enum NK_device_model NK_get_device_model() { auto m = NitrokeyManager::instance(); diff --git a/NK_C_API.h b/NK_C_API.h index d5c54a3..6aab7ca 100644 --- a/NK_C_API.h +++ b/NK_C_API.h @@ -265,6 +265,32 @@ extern "C" { uint8_t write_level_max; }; + /** + * The general configuration of a Nitrokey device. + */ + struct NK_config { + /** + * value in range [0-1] to send HOTP code from slot 'numlock' after double pressing numlock + * or outside the range to disable this function + */ + uint8_t numlock; + /** + * similar to numlock but with capslock + */ + uint8_t capslock; + /** + * similar to numlock but with scrolllock + */ + uint8_t scrolllock; + /** + * True to enable OTP PIN protection (require PIN each OTP code request) + */ + bool enable_user_password; + /** + * Unused. + */ + bool disable_user_password; + }; struct NK_storage_ProductionTest{ uint8_t FirmwareVersion_au8[2]; @@ -449,6 +475,14 @@ extern "C" { NK_C_API int NK_write_config(uint8_t numlock, uint8_t capslock, uint8_t scrolllock, bool enable_user_password, bool delete_user_password, const char *admin_temporary_password); + /** + * Write general config to the device + * @param config the configuration data + * @param admin_temporary_password current admin temporary password + * @return command processing error code + */ + NK_C_API int NK_write_config_struct(struct NK_config config, const char *admin_temporary_password); + /** * Get currently set config - status of function Numlock/Capslock/Scrollock OTP sending and is enabled PIN protected OTP * @see NK_write_config @@ -462,6 +496,15 @@ extern "C" { */ NK_C_API uint8_t* NK_read_config(); + /** + * Get currently set config and write it to the given pointer. + * @see NK_read_config + * @see NK_write_config_struct + * @param out a pointer to the struct that should be written to + * @return command processing error code + */ + NK_C_API int NK_read_config_struct(struct NK_config* out); + //OTP /** diff --git a/unittest/test_pro.py b/unittest/test_pro.py index 99d7b1f..e61d8bf 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -647,6 +647,30 @@ def test_read_write_config(C): config = cast_pointer_to_tuple(config_raw_data, 'uint8_t', 5) assert config == (0, 1, 2, True, False) + # use structs: read I + config_st = ffi.new('struct NK_config *') + if not config_st: + raise Exception("Could not allocate config") + assert C.NK_read_config_struct(config_st) == DeviceErrorCode.STATUS_OK + assert config_st.numlock == 0 + assert config_st.capslock == 1 + assert config_st.scrolllock == 2 + assert config_st.enable_user_password + assert not config_st.disable_user_password + + # use structs: write + config_st.numlock = 3 + assert C.NK_write_config_struct(config_st[0], DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + + # use structs: read II + err = C.NK_read_config_struct(config_st) + assert err == 0 + assert config_st.numlock == 3 + assert config_st.capslock == 1 + assert config_st.scrolllock == 2 + assert config_st.enable_user_password + assert not config_st.disable_user_password + # restore defaults and check assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK assert C.NK_write_config(255, 255, 255, False, True, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK @@ -1038,4 +1062,4 @@ def test_OTP_all_rw(C): this_loop_codes.append(('H', i, code)) all_codes.append(this_loop_codes) from pprint import pprint - pprint(all_codes) \ No newline at end of file + pprint(all_codes) -- cgit v1.2.1 From 2e38681cd0b34e1ce36a6417445f3a7ca75f246c Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Thu, 16 Apr 2020 11:23:10 +0200 Subject: Fix build warning - possibly uninitialized value --- libnitrokey/device_proto.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libnitrokey/device_proto.h b/libnitrokey/device_proto.h index 45a6c16..6ffe5fb 100644 --- a/libnitrokey/device_proto.h +++ b/libnitrokey/device_proto.h @@ -249,7 +249,7 @@ namespace nitrokey { } dev->m_counters.total_comm_runs++; - int status; + int status = 0; OutgoingPacket outp; ResponsePacket resp; -- cgit v1.2.1 From 5f0d4ace04ec1c99e5e851cf79559c80c5f7f379 Mon Sep 17 00:00:00 2001 From: Sven Ebenfeld Date: Tue, 28 Apr 2020 15:03:33 +0200 Subject: add python_bindings_example for python3 Signed-off-by: Sven Ebenfeld --- python3_bindings_example.py | 157 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 python3_bindings_example.py diff --git a/python3_bindings_example.py b/python3_bindings_example.py new file mode 100644 index 0000000..fb24eff --- /dev/null +++ b/python3_bindings_example.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +""" +Copyright (c) 2015-2018 Nitrokey UG + +This file is part of libnitrokey. + +libnitrokey is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +any later version. + +libnitrokey is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with libnitrokey. If not, see . + +SPDX-License-Identifier: LGPL-3.0 +""" + +import cffi +from enum import Enum + +""" +This example will print 10 HOTP codes from just written HOTP#2 slot. +For more examples of use please refer to unittest/test_*.py files. +""" + +ffi = cffi.FFI() +get_string = ffi.string + +class DeviceErrorCode(Enum): + STATUS_OK = 0 + NOT_PROGRAMMED = 3 + WRONG_PASSWORD = 4 + STATUS_NOT_AUTHORIZED = 5 + STATUS_AES_DEC_FAILED = 0xa + + +def get_library(): + fp = 'NK_C_API.h' # path to C API header + + declarations = [] + with open(fp, 'r') as f: + declarations = f.readlines() + + cnt = 0 + a = iter(declarations) + for declaration in a: + if declaration.strip().startswith('NK_C_API'): + declaration = declaration.replace('NK_C_API', '').strip() + while ';' not in declaration: + declaration += (next(a)).strip() + # print(declaration) + ffi.cdef(declaration, override=True) + cnt +=1 + print('Imported {} declarations'.format(cnt)) + + + C = None + import os, sys + path_build = os.path.join(".", "build") + paths = [ + os.environ.get('LIBNK_PATH', None), + os.path.join(path_build,"libnitrokey.so"), + os.path.join(path_build,"libnitrokey.dylib"), + os.path.join(path_build,"libnitrokey.dll"), + os.path.join(path_build,"nitrokey.dll"), + ] + for p in paths: + if not p: continue + print("Trying " +p) + p = os.path.abspath(p) + if os.path.exists(p): + print("Found: "+p) + C = ffi.dlopen(p) + break + else: + print("File does not exist: " + p) + if not C: + print("No library file found") + print("Please set the path using LIBNK_PATH environment variable to existing library or compile it (see " + "README.md for details)") + sys.exit(1) + + return C + + +def get_hotp_code(lib, i): + return get_string(lib.NK_get_hotp_code(i)) + +def to_hex(ss): + return ''.join([ format(ord(s),'02x') for s in ss ]) + +print('Warning!') +print('This example will change your configuration on inserted stick and overwrite your HOTP#2 slot.') +print('Please write "continue" to continue or any other string to quit') +a = input() + +if not a == 'continue': + exit() + +ADMIN = input('Please enter your admin PIN (empty string uses 12345678) ') +ADMIN = ADMIN or '12345678' # use default if empty string + +show_log = input('Should log messages be shown (please write "yes" to enable; this will make harder reading script output) ') == 'yes' +libnitrokey = get_library() + +if show_log: + log_level = input('Please select verbosity level (0-5, 2 is library default, 3 will be selected on empty input) ') + log_level = log_level or '3' + log_level = int(log_level) + libnitrokey.NK_set_debug_level(log_level) +else: + libnitrokey.NK_set_debug_level(2) + + +ADMIN_TEMP = '123123123' +RFC_SECRET = to_hex('12345678901234567890') + +# libnitrokey.NK_login('S') # connect only to Nitrokey Storage device +# libnitrokey.NK_login('P') # connect only to Nitrokey Pro device +device_connected = libnitrokey.NK_login_auto() # connect to any Nitrokey Stick +if device_connected: + print('Connected to Nitrokey device!') +else: + print('Could not connect to Nitrokey device!') + exit() + +use_8_digits = True +pin_correct = libnitrokey.NK_first_authenticate(ADMIN.encode('ascii'), ADMIN_TEMP.encode('ascii')) == DeviceErrorCode.STATUS_OK.value +if pin_correct: + print('Your PIN is correct!') +else: + print('Your PIN is not correct! Please try again. Please be careful to not lock your stick!') + retry_count_left = libnitrokey.NK_get_admin_retry_count() + print('Retry count left: %d' % retry_count_left ) + exit() + +# For function parameters documentation please check NK_C_API.h +assert libnitrokey.NK_write_config(255, 255, 255, False, True, ADMIN_TEMP.encode('ascii')) == DeviceErrorCode.STATUS_OK.value +libnitrokey.NK_first_authenticate(ADMIN.encode('ascii'), ADMIN_TEMP.encode('ascii')) +libnitrokey.NK_write_hotp_slot(1, 'python_test'.encode('ascii'), RFC_SECRET.encode('ascii'), 0, use_8_digits, False, False, "".encode('ascii'), + ADMIN_TEMP.encode('ascii')) +# RFC test according to: https://tools.ietf.org/html/rfc4226#page-32 +test_data = [ + 1284755224, 1094287082, 137359152, 1726969429, 1640338314, 868254676, 1918287922, 82162583, 673399871, + 645520489, +] +print('Getting HOTP code from Nitrokey Stick (RFC test, 8 digits): ') +for i in range(10): + hotp_slot_1_code = get_hotp_code(libnitrokey, 1) + correct_str = "correct!" if hotp_slot_1_code.decode('ascii') == str(test_data[i])[-8:] else "not correct" + print('%d: %s, should be %s -> %s' % (i, hotp_slot_1_code.decode('ascii'), str(test_data[i])[-8:], correct_str)) +libnitrokey.NK_logout() # disconnect device -- cgit v1.2.1 From df952bba8dac9b88f1c62efeb0bf7af9139e59bf Mon Sep 17 00:00:00 2001 From: Sven Ebenfeld Date: Tue, 28 Apr 2020 15:13:25 +0200 Subject: Add Python3 Example in README.md Signed-off-by: Sven Ebenfeld --- README.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/README.md b/README.md index a3683c0..aa5ca1a 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ To use libnitrokey with Python a [CFFI](http://cffi.readthedocs.io/en/latest/ove pip install --user cffi # for python 2.x pip3 install cffi # for python 3.x ``` +## Python2 Just import it, read the C API header and it is done! You have access to the library. Here is an example (in Python 2) printing HOTP code for Pro or Storage device, assuming it is run in root directory [(full example)](python_bindings_example.py): ```python #!/usr/bin/env python2 @@ -158,6 +159,88 @@ print('Getting HOTP code from Nitrokey device: ') print(hotp_slot_code) libnitrokey.NK_logout() # disconnect device ``` +In case no devices are connected, a friendly message will be printed. +All available functions for C and Python are listed in [NK_C_API.h](NK_C_API.h). Please check `Documentation` section below. + +## Python3 +Just import it, read the C API header and it is done! You have access to the library. Here is an example (in Python 3) printing HOTP code for Pro or Storage device, assuming it is run in root directory [(full example)](python3_bindings_example.py): +```python +#!/usr/bin/env python3 +import cffi + +ffi = cffi.FFI() +get_string = ffi.string + +def get_library(): + fp = 'NK_C_API.h' # path to C API header + + declarations = [] + with open(fp, 'r') as f: + declarations = f.readlines() + + cnt = 0 + a = iter(declarations) + for declaration in a: + if declaration.strip().startswith('NK_C_API'): + declaration = declaration.replace('NK_C_API', '').strip() + while ';' not in declaration: + declaration += (next(a)).strip() + # print(declaration) + ffi.cdef(declaration, override=True) + cnt +=1 + print('Imported {} declarations'.format(cnt)) + + + C = None + import os, sys + path_build = os.path.join(".", "build") + paths = [ + os.environ.get('LIBNK_PATH', None), + os.path.join(path_build,"libnitrokey.so"), + os.path.join(path_build,"libnitrokey.dylib"), + os.path.join(path_build,"libnitrokey.dll"), + os.path.join(path_build,"nitrokey.dll"), + ] + for p in paths: + if not p: continue + print("Trying " +p) + p = os.path.abspath(p) + if os.path.exists(p): + print("Found: "+p) + C = ffi.dlopen(p) + break + else: + print("File does not exist: " + p) + if not C: + print("No library file found") + sys.exit(1) + + return C + + +def get_hotp_code(lib, i): + return lib.NK_get_hotp_code(i) + +def connect_device(lib): + # lib.NK_login('S'.encode('ascii')) # connect only to Nitrokey Storage device + # lib.NK_login('P'.encode('ascii')) # connect only to Nitrokey Pro device + device_connected = lib.NK_login_auto() # connect to any Nitrokey Stick + if device_connected: + print('Connected to Nitrokey device!') + else: + print('Could not connect to Nitrokey device!') + exit() + +libnitrokey = get_library() +libnitrokey.NK_set_debug(False) # do not show debug messages (log library only) + +connect_device(libnitrokey) + +hotp_slot_code = get_hotp_code(libnitrokey, 1) +print('Getting HOTP code from Nitrokey device: ') +print(ffi.string(hotp_slot_code).decode('ascii')) +libnitrokey.NK_logout() # disconnect device +``` In case no devices are connected, a friendly message will be printed. All available functions for C and Python are listed in [NK_C_API.h](NK_C_API.h). Please check `Documentation` section below. -- cgit v1.2.1 From 444a6cb764fbcea3c91ae936b1c76a190f935b10 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Sat, 13 Jun 2020 12:21:44 +0200 Subject: Revert API change Remove the change to keep binary compatibility. Use the vendor_id field from the dev description. --- NitrokeyManager.cc | 2 +- device.cc | 4 ++++ libnitrokey/device.h | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/NitrokeyManager.cc b/NitrokeyManager.cc index d874dca..71d156f 100644 --- a/NitrokeyManager.cc +++ b/NitrokeyManager.cc @@ -230,7 +230,7 @@ using nitrokey::misc::strcpyT; misc::Option model; while (info_ptr && !model.has_value()) { if (path == std::string(info_ptr->path)) { - model = product_id_to_model(vendor_id, info_ptr->product_id); + model = product_id_to_model(info_ptr->vendor_id, info_ptr->product_id); } info_ptr = info_ptr->next; } diff --git a/device.cc b/device.cc index 5f89f2c..9cf4dc3 100644 --- a/device.cc +++ b/device.cc @@ -48,6 +48,10 @@ const uint16_t nitrokey::device::NITROKEY_STORAGE_PID = 0x4109; const uint16_t nitrokey::device::PURISM_VID = 0x316d; const uint16_t nitrokey::device::LIBREM_KEY_PID = 0x4c4b; +Option nitrokey::device::product_id_to_model(uint16_t product_id) { + return product_id_to_model(NITROKEY_VID, product_id); +} + Option nitrokey::device::product_id_to_model(uint16_t vendor_id, uint16_t product_id) { switch (vendor_id) { case NITROKEY_VID: diff --git a/libnitrokey/device.h b/libnitrokey/device.h index d39310a..917e0d0 100644 --- a/libnitrokey/device.h +++ b/libnitrokey/device.h @@ -81,6 +81,7 @@ extern const uint16_t LIBREM_KEY_PID; * Convert the given USB product ID to a Nitrokey model. If there is no model * with that ID, return an absent value. */ +misc::Option product_id_to_model(uint16_t product_id); misc::Option product_id_to_model(uint16_t vendor_id, uint16_t product_id); /** -- cgit v1.2.1 From 24fdcd777d0144e00038aad3a572c5922e9de710 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Sat, 13 Jun 2020 18:08:06 +0200 Subject: Update build configurations --- .travis.yml | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6bf5438..703a4bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,8 @@ env: matrix: include: -# - osx_image: xcode7.3 #default -# before_install: &brew -# - brew update -# - brew install hidapi + - osx_image: xcode11.5 - osx_image: xcode9.1 - - osx_image: xcode8.2 - os: linux dist: trusty env: COMPILER_NAME=gcc CXX=g++-5 CC=gcc-5 @@ -38,23 +34,23 @@ matrix: - cd unittest && python3 -m pytest -sv test_offline.py - os: linux dist: trusty - env: COMPILER_NAME=gcc CXX=g++-6 CC=gcc-6 + env: COMPILER_NAME=gcc CXX=g++-7 CC=gcc-7 addons: apt: packages: - cmake - libhidapi-dev - - g++-6 + - g++-7 sources: *sources - os: linux - dist: trusty - env: COMPILER_NAME=gcc CXX=g++-7 CC=gcc-7 + dist: bionic + env: COMPILER_NAME=gcc CXX=g++-10 CC=gcc-10 addons: apt: packages: - cmake - libhidapi-dev - - g++-7 + - g++-10 sources: *sources - os: linux dist: trusty @@ -67,6 +63,26 @@ matrix: - g++-5 - clang-3.8 sources: *sources + - os: linux + dist: bionic + env: COMPILER_NAME=clang CXX=clang++-6.0 CC=clang-6.0 + addons: + apt: + packages: + - cmake + - libhidapi-dev + - clang-6.0 + sources: *sources + - os: linux + dist: bionic + env: COMPILER_NAME=clang CXX=clang++-9 CC=clang-9 + addons: + apt: + packages: + - cmake + - libhidapi-dev + - clang-9 + sources: *sources install: -- cgit v1.2.1 From dc234d158f27a5aa385c80daa63365b3c5472944 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Sat, 13 Jun 2020 18:23:27 +0200 Subject: Move offline Python tests call to newer Ubuntu --- .travis.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 703a4bb..0ad1575 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,14 +24,6 @@ matrix: - git sources: &sources - ubuntu-toolchain-r-test - script: - - make -j2 - - ctest -VV - - mkdir install && make install DESTDIR=install - - pip3 install pytest --user - - cd ../ - - pip3 install -r unittest/requirements.txt --user - - cd unittest && python3 -m pytest -sv test_offline.py - os: linux dist: trusty env: COMPILER_NAME=gcc CXX=g++-7 CC=gcc-7 @@ -52,6 +44,14 @@ matrix: - libhidapi-dev - g++-10 sources: *sources + script: + - make -j2 + - ctest -VV + - mkdir install && make install DESTDIR=install + - pip3 install pytest --user + - cd ../ + - pip3 install -r unittest/requirements.txt --user + - cd unittest && python3 -m pytest -sv test_offline.py - os: linux dist: trusty env: COMPILER_NAME=clang CXX=clang++-3.8 CC=clang-3.8 -- cgit v1.2.1 From f3cffe7245f25d6a8b0651987a9e412c374027e9 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Sat, 13 Jun 2020 18:29:58 +0200 Subject: Make proper call to install pip package --- .travis.yml | 9 +++------ unittest/requirements.txt | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0ad1575..52a10e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,10 +18,6 @@ matrix: - cmake - libhidapi-dev - g++-5 - - python3 - - python3-pip - - python3-requests - - git sources: &sources - ubuntu-toolchain-r-test - os: linux @@ -43,14 +39,15 @@ matrix: - cmake - libhidapi-dev - g++-10 + - python3 + - python3-pip sources: *sources script: - make -j2 - ctest -VV - mkdir install && make install DESTDIR=install - - pip3 install pytest --user - cd ../ - - pip3 install -r unittest/requirements.txt --user + - python3 -m pip install -r unittest/requirements.txt --user - cd unittest && python3 -m pytest -sv test_offline.py - os: linux dist: trusty diff --git a/unittest/requirements.txt b/unittest/requirements.txt index 6d718ad..d8a7e29 100644 --- a/unittest/requirements.txt +++ b/unittest/requirements.txt @@ -1,4 +1,5 @@ cffi +pytest pytest-repeat pytest-randomly tqdm -- cgit v1.2.1 From a3fd24ff87eeae3d320e3ee48f1c6e1b9ba0a783 Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Sat, 13 Jun 2020 18:55:34 +0200 Subject: Invert if condition for readability --- device.cc | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/device.cc b/device.cc index 5f26d65..1352bc7 100644 --- a/device.cc +++ b/device.cc @@ -209,16 +209,17 @@ std::vector Device::enumerate(){ auto pInfo_ = pInfo; std::vector res; while (pInfo != nullptr){ - if (pInfo->path != nullptr && pInfo->serial_number != nullptr) { - auto deviceModel = product_id_to_model(pInfo->product_id); - if (deviceModel.has_value()) { - std::string path(pInfo->path); - std::wstring serialNumberW(pInfo->serial_number); - std::wstring_convert> converter; - std::string serialNumber = converter.to_bytes(serialNumberW); - DeviceInfo info = { deviceModel.value(), path, serialNumber }; - res.push_back(info); - } + if (pInfo->path == nullptr || pInfo->serial_number == nullptr) { + continue; + } + auto deviceModel = product_id_to_model(pInfo->product_id); + if (deviceModel.has_value()) { + std::string path(pInfo->path); + std::wstring serialNumberW(pInfo->serial_number); + std::wstring_convert> converter; + std::string serialNumber = converter.to_bytes(serialNumberW); + DeviceInfo info = {deviceModel.value(), path, serialNumber}; + res.push_back(info); } pInfo = pInfo->next; } -- cgit v1.2.1