diff options
author | Szczepan Zalega <szczepan@nitrokey.com> | 2019-01-15 16:02:31 +0100 |
---|---|---|
committer | Szczepan Zalega <szczepan@nitrokey.com> | 2019-01-15 16:02:31 +0100 |
commit | e853275784e48823be0f39f0855efbe85c0f905c (patch) | |
tree | 2b428a23bddbd87c08ee47351bc31768f54b63fe | |
parent | 3f3fdebbc795dc3805cd5be105ce994286598f16 (diff) | |
parent | cd1cfdbfc4113186f80dbadf5eb76543b22e34bd (diff) | |
download | libnitrokey-e853275784e48823be0f39f0855efbe85c0f905c.tar.gz libnitrokey-e853275784e48823be0f39f0855efbe85c0f905c.tar.bz2 |
Merge branch 'pr_138' into contributions
Improve support for multiple devices
Fixes #138
-rw-r--r-- | NK_C_API.cc | 68 | ||||
-rw-r--r-- | NK_C_API.h | 44 | ||||
-rw-r--r-- | NitrokeyManager.cc | 37 | ||||
-rw-r--r-- | device.cc | 66 | ||||
-rw-r--r-- | libnitrokey/NitrokeyManager.h | 2 | ||||
-rw-r--r-- | libnitrokey/device.h | 57 | ||||
-rw-r--r-- | libnitrokey/misc.h | 25 | ||||
-rw-r--r-- | unittest/test_multiple.py | 29 | ||||
-rw-r--r-- | unittest/test_multiple_devices.cc | 81 |
9 files changed, 385 insertions, 24 deletions
diff --git a/NK_C_API.cc b/NK_C_API.cc index a780acf..794673a 100644 --- a/NK_C_API.cc +++ b/NK_C_API.cc @@ -753,6 +753,66 @@ NK_C_API char* NK_get_SD_usage_data_as_string() { }); } + bool copy_device_info(const DeviceInfo& source, NK_device_info* target) { + switch (source.m_deviceModel) { + case DeviceModel::PRO: + target->model = NK_PRO; + break; + case DeviceModel::STORAGE: + target->model = NK_STORAGE; + break; + default: + return false; + } + + target->path = strndup(source.m_path.c_str(), MAXIMUM_STR_REPLY_LENGTH); + target->serial_number = strndup(source.m_serialNumber.c_str(), MAXIMUM_STR_REPLY_LENGTH); + target->next = nullptr; + + return target->path && target->serial_number; + } + + NK_C_API struct NK_device_info* NK_list_devices() { + auto nm = NitrokeyManager::instance(); + return get_with_result([&]() -> NK_device_info* { + auto v = nm->list_devices(); + if (v.empty()) + return nullptr; + + auto result = new NK_device_info(); + auto ptr = result; + auto first = v.begin(); + if (!copy_device_info(*first, ptr)) { + NK_free_device_info(result); + return nullptr; + } + v.erase(first); + + for (auto& info : v) { + ptr->next = new NK_device_info(); + ptr = ptr->next; + + if (!copy_device_info(info, ptr)) { + NK_free_device_info(result); + return nullptr; + } + } + return result; + }); + } + + NK_C_API void NK_free_device_info(struct NK_device_info* device_info) { + if (!device_info) + return; + + if (device_info->next) + NK_free_device_info(device_info->next); + + free(device_info->path); + free(device_info->serial_number); + delete device_info; + } + NK_C_API int NK_connect_with_ID(const char* id) { auto m = NitrokeyManager::instance(); return get_with_result([&]() { @@ -760,6 +820,14 @@ NK_C_API char* NK_get_SD_usage_data_as_string() { }); } + NK_C_API int NK_connect_with_path(const char* path) { + auto m = NitrokeyManager::instance(); + return get_with_result([&]() { + return m->connect_with_path(path) ? 1 : 0; + }); + } + + NK_C_API int NK_wink() { auto m = NitrokeyManager::instance(); return get_without_result([&]() { @@ -57,6 +57,29 @@ extern "C" { NK_STORAGE = 2 }; + /** + * The connection info for a Nitrokey device as a linked list. + */ + struct NK_device_info { + /** + * The model of the Nitrokey device. + */ + enum NK_device_model model; + /** + * The USB device path for NK_connect_with_path. + */ + char* path; + /** + * The serial number. + */ + char* serial_number; + /** + * The pointer to the next element of the linked list or null + * if this is the last element in the list. + */ + struct NK_device_info* next; + }; + /** * Stores the status of a Storage device. */ @@ -797,6 +820,19 @@ extern "C" { */ NK_C_API char* NK_list_devices_by_cpuID(); + /** + * Returns a linked list of all connected devices, or null if no devices + * are connected or an error occured. The linked list must be freed by + * calling NK_free_device_info. + * @return a linked list of all connected devices + */ + NK_C_API struct NK_device_info* NK_list_devices(); + + /** + * Free a linked list returned by NK_list_devices. + * @param the linked list to free or null + */ + NK_C_API void NK_free_device_info(struct NK_device_info* device_info); /** * Connects to the device with given ID. ID's list could be created with NK_list_devices_by_cpuID. @@ -809,6 +845,14 @@ extern "C" { NK_C_API int NK_connect_with_ID(const char* id); /** + * Connects to a device with the given path. The path is a USB device + * path as returned by hidapi. + * @param path the device path + * @return 1 on successful connection, 0 otherwise + */ + NK_C_API int NK_connect_with_path(const char* path); + + /** * Blink red and green LED alternatively and infinitely (until device is reconnected). * @return command processing error code */ diff --git a/NitrokeyManager.cc b/NitrokeyManager.cc index a950e4b..99f0b7a 100644 --- a/NitrokeyManager.cc +++ b/NitrokeyManager.cc @@ -29,6 +29,7 @@ #include "libnitrokey/misc.h" #include <mutex> #include "libnitrokey/cxx_semantics.h" +#include "libnitrokey/misc.h" #include <functional> #include <stick10_commands.h> @@ -105,11 +106,10 @@ using nitrokey::misc::strcpyT; return true; } - std::vector<std::string> NitrokeyManager::list_devices(){ + std::vector<DeviceInfo> NitrokeyManager::list_devices(){ std::lock_guard<std::mutex> lock(mex_dev_com_manager); - auto p = make_shared<Stick20>(); - return p->enumerate(); // make static + return Device::enumerate(); } std::vector<std::string> NitrokeyManager::list_devices_by_cpuID(){ @@ -127,11 +127,13 @@ using nitrokey::misc::strcpyT; LOGD1("Enumerating devices"); std::vector<std::string> res; - auto d = make_shared<Stick20>(); - const auto v = d->enumerate(); + const auto v = Device::enumerate(); LOGD1("Discovering IDs"); - for (auto & p: v){ - d = make_shared<Stick20>(); + for (auto & i: v){ + if (i.m_deviceModel != DeviceModel::STORAGE) + continue; + auto p = i.m_path; + auto d = make_shared<Stick20>(); LOGD1( std::string("Found: ") + p ); d->set_path(p); try{ @@ -215,7 +217,26 @@ using nitrokey::misc::strcpyT; } } - auto p = make_shared<Stick20>(); + auto info_ptr = hid_enumerate(NITROKEY_VID, 0); + auto first_info_ptr = info_ptr; + if (!info_ptr) + return false; + + misc::Option<DeviceModel> model; + while (info_ptr && !model.has_value()) { + if (path == std::string(info_ptr->path)) { + model = product_id_to_model(info_ptr->product_id); + } + info_ptr = info_ptr->next; + } + hid_free_enumeration(first_info_ptr); + + if (!model.has_value()) + return false; + + auto p = Device::create(model.value()); + if (!p) + return false; p->set_path(path); if(!p->connect()) return false; @@ -20,7 +20,9 @@ */ #include <chrono> +#include <codecvt> #include <iostream> +#include <locale> #include <thread> #include <cstddef> #include <stdexcept> @@ -36,11 +38,42 @@ std::mutex mex_dev_com; using namespace nitrokey::device; using namespace nitrokey::log; +using namespace nitrokey::misc; using namespace std::chrono; +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<DeviceModel> nitrokey::device::product_id_to_model(uint16_t product_id) { + switch (product_id) { + case NITROKEY_PRO_PID: + return DeviceModel::PRO; + case NITROKEY_STORAGE_PID: + return DeviceModel::STORAGE; + default: + return {}; + } +} + std::atomic_int Device::instances_count{0}; std::chrono::milliseconds Device::default_delay {0} ; +std::ostream& nitrokey::device::operator<<(std::ostream& stream, DeviceModel model) { + switch (model) { + case DeviceModel::PRO: + stream << "Pro"; + break; + case DeviceModel::STORAGE: + stream << "Storage"; + break; + default: + stream << "Unknown"; + break; + } + return stream; +} + Device::Device(const uint16_t vid, const uint16_t pid, const DeviceModel model, const milliseconds send_receive_delay, const int retry_receiving_count, const milliseconds retry_timeout) @@ -171,14 +204,20 @@ int Device::recv(void *packet) { return status; } -std::vector<std::string> Device::enumerate(){ - //TODO make static - auto pInfo = hid_enumerate(m_vid, m_pid); +std::vector<DeviceInfo> Device::enumerate(){ + auto pInfo = hid_enumerate(NITROKEY_VID, 0); auto pInfo_ = pInfo; - std::vector<std::string> res; + std::vector<DeviceInfo> res; while (pInfo != nullptr){ - std::string a (pInfo->path); - res.push_back(a); + 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<std::codecvt_utf8<wchar_t>> converter; + std::string serialNumber = converter.to_bytes(serialNumberW); + DeviceInfo info = { deviceModel.value(), path, serialNumber }; + res.push_back(info); + } pInfo = pInfo->next; } @@ -189,6 +228,17 @@ std::vector<std::string> Device::enumerate(){ return res; } +std::shared_ptr<Device> Device::create(DeviceModel model) { + switch (model) { + case DeviceModel::PRO: + return std::make_shared<Stick10>(); + case DeviceModel::STORAGE: + return std::make_shared<Stick20>(); + default: + return {}; + } +} + bool Device::could_be_enumerated() { LOG(__FUNCTION__, Loglevel::DEBUG_L2); std::lock_guard<std::mutex> lock(mex_dev_com); @@ -243,14 +293,14 @@ void Device::set_retry_delay(const std::chrono::milliseconds delay){ } Stick10::Stick10(): - Device(0x20a0, 0x4108, DeviceModel::PRO, 100ms, 5, 100ms) + Device(NITROKEY_VID, NITROKEY_PRO_PID, DeviceModel::PRO, 100ms, 5, 100ms) { setDefaultDelay(); } Stick20::Stick20(): - Device(0x20a0, 0x4109, DeviceModel::STORAGE, 40ms, 55, 40ms) + Device(NITROKEY_VID, NITROKEY_STORAGE_PID, DeviceModel::STORAGE, 40ms, 55, 40ms) { setDefaultDelay(); } diff --git a/libnitrokey/NitrokeyManager.h b/libnitrokey/NitrokeyManager.h index d6e5df4..6908143 100644 --- a/libnitrokey/NitrokeyManager.h +++ b/libnitrokey/NitrokeyManager.h @@ -80,7 +80,7 @@ char * strndup(const char* str, size_t maxlen); bool get_time(uint64_t time = 0); bool erase_totp_slot(uint8_t slot_number, const char *temporary_password); bool erase_hotp_slot(uint8_t slot_number, const char *temporary_password); - std::vector<std::string> list_devices(); + std::vector<DeviceInfo> list_devices(); std::vector<std::string> list_devices_by_cpuID(); /** diff --git a/libnitrokey/device.h b/libnitrokey/device.h index f6d2380..d50080d 100644 --- a/libnitrokey/device.h +++ b/libnitrokey/device.h @@ -24,8 +24,11 @@ #include <chrono> #include "hidapi/hidapi.h" #include <cstdint> +#include <memory> #include <string> +#include <ostream> #include <vector> +#include "misc.h" #define HID_REPORT_SIZE 65 @@ -50,6 +53,48 @@ enum class DeviceModel{ STORAGE }; +std::ostream& operator<<(std::ostream& stream, DeviceModel model); + +/** + * The USB vendor ID for Nitrokey devices. + */ +extern const uint16_t NITROKEY_VID; +/** + * The USB product ID for the Nitrokey Pro. + */ +extern const uint16_t NITROKEY_PRO_PID; +/** + * The USB product ID for the Nitrokey Storage. + */ +extern const uint16_t NITROKEY_STORAGE_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<DeviceModel> product_id_to_model(uint16_t product_id); + +/** + * Information about a connected device. + * + * This struct contains the information about a connected device returned by + * hidapi when enumerating the connected devices. + */ +struct DeviceInfo { + /** + * The model of the connected device. + */ + DeviceModel m_deviceModel; + /** + * The USB connection path for the device. + */ + std::string m_path; + /** + * The serial number of the device. + */ + std::string m_serialNumber; +}; + #include <atomic> class Device { @@ -106,7 +151,17 @@ public: * @return true if visible by OS */ bool could_be_enumerated(); - std::vector<std::string> enumerate(); + /** + * Returns a vector with all connected Nitrokey devices. + * + * @return information about all connected devices + */ + static std::vector<DeviceInfo> enumerate(); + + /** + * Create a Device of the given model. + */ + static std::shared_ptr<Device> create(DeviceModel model); void show_stats(); diff --git a/libnitrokey/misc.h b/libnitrokey/misc.h index 88254dd..d10c8df 100644 --- a/libnitrokey/misc.h +++ b/libnitrokey/misc.h @@ -29,12 +29,37 @@ #include "log.h" #include "LibraryException.h" #include <sstream> +#include <stdexcept> #include <iomanip> namespace nitrokey { namespace misc { +/** + * Simple replacement for std::optional (C++17). + */ +template<typename T> +class Option { +public: + Option() : m_hasValue(false), m_value() {} + Option(T value) : m_hasValue(true), m_value(value) {} + + bool has_value() const { + return m_hasValue; + } + T value() const { + if (!m_hasValue) { + throw std::logic_error("Called Option::value without value"); + } + return m_value; + } + +private: + bool m_hasValue; + T m_value; +}; + template<typename T> std::string toHex(T value){ using namespace std; diff --git a/unittest/test_multiple.py b/unittest/test_multiple.py index 3f1d2d5..821a3b7 100644 --- a/unittest/test_multiple.py +++ b/unittest/test_multiple.py @@ -29,13 +29,40 @@ from tqdm import tqdm from conftest import skip_if_device_version_lower_than from constants import DefaultPasswords, DeviceErrorCode, bb -from misc import gs, wait +from misc import gs, wait, ffi pprint = pprint.PrettyPrinter(indent=4).pprint @pytest.mark.other @pytest.mark.info +def test_list_devices(C): + infos = C.NK_list_devices() + assert infos != ffi.NULL + C.NK_free_device_info(infos) + + +@pytest.mark.other +@pytest.mark.info +def test_connect_with_path(C): + ids = gs(C.NK_list_devices_by_cpuID()) + # NK_list_devices_by_cpuID already opened the devices, so we have to close + # them before trying to reconnect + assert C.NK_logout() == 0 + + devices_list = ids.split(b';') + for value in devices_list: + parts = value.split(b'_p_') + assert len(parts) < 3 + if len(parts) == 2: + path = parts[1] + else: + path = parts[0] + assert C.NK_connect_with_path(path) == 1 + + +@pytest.mark.other +@pytest.mark.info def test_get_status_storage_multiple(C): ids = gs(C.NK_list_devices_by_cpuID()) print(ids) diff --git a/unittest/test_multiple_devices.cc b/unittest/test_multiple_devices.cc index cd78681..4b1e2c1 100644 --- a/unittest/test_multiple_devices.cc +++ b/unittest/test_multiple_devices.cc @@ -29,15 +29,40 @@ const char * RFC_SECRET = "12345678901234567890"; #include <iostream> #include <NitrokeyManager.h> #include <stick20_commands.h> +#include "../NK_C_API.h" using namespace nitrokey; TEST_CASE("List devices", "[BASIC]") { + auto v = Device::enumerate(); + REQUIRE(v.size() > 0); + for (auto i : v){ + auto d = Device::create(i.m_deviceModel); + if (!d) { + std::cout << "Could not create device with model " << i.m_deviceModel << "\n"; + continue; + } + std::cout << i.m_deviceModel << " " << i.m_path << " " + << i.m_serialNumber << " |"; + d->set_path(i.m_path); + d->connect(); + auto res = GetStatus::CommandTransaction::run(d); + std::cout << " " << res.data().card_serial_u32 << " " + << res.data().get_card_serial_hex() + << std::endl; + d->disconnect(); + } +} + +TEST_CASE("List Storage devices", "[BASIC]") { shared_ptr<Stick20> d = make_shared<Stick20>(); - auto v = d->enumerate(); + auto v = Device::enumerate(); REQUIRE(v.size() > 0); - for (auto a : v){ + for (auto i : v){ + if (i.m_deviceModel != DeviceModel::STORAGE) + continue; + auto a = i.m_path; std::cout << a; d->set_path(a); d->connect(); @@ -53,11 +78,14 @@ TEST_CASE("List devices", "[BASIC]") { TEST_CASE("Regenerate AES keys", "[BASIC]") { shared_ptr<Stick20> d = make_shared<Stick20>(); - auto v = d->enumerate(); + auto v = Device::enumerate(); REQUIRE(v.size() > 0); std::vector<shared_ptr<Stick20>> devices; - for (auto a : v){ + for (auto i : v){ + if (i.m_deviceModel != DeviceModel::STORAGE) + continue; + auto a = i.m_path; std::cout << a << endl; d = make_shared<Stick20>(); d->set_path(a); @@ -81,14 +109,57 @@ TEST_CASE("Regenerate AES keys", "[BASIC]") { } +TEST_CASE("Use C API", "[BASIC]") { + auto ptr = NK_list_devices(); + auto first_ptr = ptr; + REQUIRE(ptr != nullptr); + + while (ptr) { + std::cout << "Connect with: " << ptr->model << " " << ptr->path << " " + << ptr->serial_number << " | " << NK_connect_with_path(ptr->path) << " | "; + auto status = NK_status(); + std::cout << status << std::endl; + free(status); + ptr = ptr->next; + } + + NK_free_device_info(first_ptr); +} + + TEST_CASE("Use API", "[BASIC]") { auto nm = NitrokeyManager::instance(); nm->set_loglevel(2); auto v = nm->list_devices(); REQUIRE(v.size() > 0); + for (auto i : v) { + std::cout << "Connect with: " << i.m_deviceModel << " " << i.m_path << " " + << i.m_serialNumber << " | " << std::boolalpha << nm->connect_with_path(i.m_path) << " |"; + try { + auto status = nm->get_status(); + std::cout << " " << status.card_serial_u32 << " " + << status.get_card_serial_hex() + << std::endl; + } catch (const LongOperationInProgressException &e) { + std::cout << "long operation in progress on " << i.m_path + << " " << std::to_string(e.progress_bar_value) << std::endl; + } + } +} + + +TEST_CASE("Use Storage API", "[BASIC]") { + auto nm = NitrokeyManager::instance(); + nm->set_loglevel(2); + auto v = nm->list_devices(); + REQUIRE(v.size() > 0); + for (int i=0; i<10; i++){ - for (auto a : v) { + for (auto i : v) { + if (i.m_deviceModel != DeviceModel::STORAGE) + continue; + auto a = i.m_path; std::cout <<"Connect with: " << a << " " << std::boolalpha << nm->connect_with_path(a) << " "; try{ |