diff options
author | szszszsz <szszszsz@users.noreply.github.com> | 2016-12-12 17:06:35 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-12-12 17:06:35 +0100 |
commit | ed5044da43172d86a1aa475473561a4818b7c69c (patch) | |
tree | a6d3775f20ac86e7cdbbc151e0f51620d1399e56 | |
parent | f60f2cf0144a91769a5fc00fac1314d2e00cdf0d (diff) | |
parent | e26c6da38c674d8ec37e402132dab823bd22bd36 (diff) | |
download | libnitrokey-2.0.tar.gz libnitrokey-2.0.tar.bz2 |
Merge pull request #53 from Nitrokey/nk_pro_0.8_authorization_fix-longer_secretv2.0
Support for Nitrokey Pro 0.8
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | NK_C_API.cc | 7 | ||||
-rw-r--r-- | NK_C_API.h | 6 | ||||
-rw-r--r-- | NitrokeyManager.cc | 237 | ||||
-rw-r--r-- | command_id.cc | 6 | ||||
-rw-r--r-- | include/NitrokeyManager.h | 20 | ||||
-rw-r--r-- | include/command_id.h | 2 | ||||
-rw-r--r-- | include/device.h | 9 | ||||
-rw-r--r-- | include/dissect.h | 59 | ||||
-rw-r--r-- | include/stick10_commands_0.8.h | 312 | ||||
-rw-r--r-- | include/stick20_commands.h | 10 | ||||
-rw-r--r-- | misc.cc | 7 | ||||
-rw-r--r-- | unittest/conftest.py | 16 | ||||
-rw-r--r-- | unittest/constants.py | 2 | ||||
-rw-r--r-- | unittest/misc.py | 24 | ||||
-rw-r--r-- | unittest/requirements.txt | 4 | ||||
-rw-r--r-- | unittest/test3.cc | 220 | ||||
-rw-r--r-- | unittest/test_library.py | 15 | ||||
-rw-r--r-- | unittest/test_pro.py | 276 | ||||
-rw-r--r-- | unittest/test_storage.py | 22 |
20 files changed, 1149 insertions, 107 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index e9cd54f..c324067 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,9 @@ set(SOURCE_FILES unittest/test_C_API.cpp unittest/catch_main.cpp unittest/test2.cc + unittest/test3.cc include/LongOperationInProgressException.h + include/stick10_commands_0.8.h ) add_executable(libnitrokey ${SOURCE_FILES})
\ No newline at end of file diff --git a/NK_C_API.cc b/NK_C_API.cc index d42840b..e513a3b 100644 --- a/NK_C_API.cc +++ b/NK_C_API.cc @@ -471,6 +471,13 @@ extern int NK_get_progress_bar_value() { }); } +extern int NK_get_major_firmware_version(){ + auto m = NitrokeyManager::instance(); + return get_with_result([&](){ + return m->get_major_firmware_version(); + }); +} + } @@ -324,7 +324,11 @@ extern int NK_erase_password_safe_slot(uint8_t slot_number); */ extern int NK_is_AES_supported(const char *user_password); - +/** + * Get device's major firmware version + * @return 7,8 for Pro and major for Storage + */ +extern int NK_get_major_firmware_version(); diff --git a/NitrokeyManager.cc b/NitrokeyManager.cc index 20f4f14..ddec600 100644 --- a/NitrokeyManager.cc +++ b/NitrokeyManager.cc @@ -3,6 +3,8 @@ #include "include/NitrokeyManager.h" #include "include/LibraryException.h" #include <algorithm> +#include <unordered_map> +#include <stick20_commands.h> #include "include/misc.h" namespace nitrokey{ @@ -35,7 +37,10 @@ namespace nitrokey{ // package type to auth, auth type [Authorize,UserAuthorize] template <typename S, typename A, typename T> - void authorize_packet(T &package, const char *admin_temporary_password, shared_ptr<Device> device){ + void NitrokeyManager::authorize_packet(T &package, const char *admin_temporary_password, shared_ptr<Device> device){ + if (!is_authorization_command_supported()){ + Log::instance()("Authorization command not supported, skipping", Loglevel::WARNING); + } auto auth = get_payload<A>(); strcpyT(auth.temporary_password, admin_temporary_password); auth.crc_to_authorize = S::CommandTransaction::getCRC(package); @@ -112,16 +117,25 @@ namespace nitrokey{ } uint32_t NitrokeyManager::get_HOTP_code(uint8_t slot_number, const char *user_temporary_password) { - if (!is_valid_hotp_slot_number(slot_number)) throw InvalidSlotException(slot_number); + if (!is_valid_hotp_slot_number(slot_number)) throw InvalidSlotException(slot_number); + + if (is_authorization_command_supported()){ auto gh = get_payload<GetHOTP>(); gh.slot_number = get_internal_slot_number_for_hotp(slot_number); - if(user_temporary_password != nullptr && strlen(user_temporary_password)!=0){ //FIXME use string instead of strlen authorize_packet<GetHOTP, UserAuthorize>(gh, user_temporary_password, device); } - auto resp = GetHOTP::CommandTransaction::run(*device, gh); return resp.data().code; + } else { + auto gh = get_payload<stick10_08::GetHOTP>(); + gh.slot_number = get_internal_slot_number_for_hotp(slot_number); + if(user_temporary_password != nullptr && strlen(user_temporary_password)!=0) { + strcpyT(gh.temporary_user_password, user_temporary_password); + } + auto resp = stick10_08::GetHOTP::CommandTransaction::run(*device, gh); + return resp.data().code; + } } @@ -135,26 +149,41 @@ namespace nitrokey{ const char *user_temporary_password) { if(!is_valid_totp_slot_number(slot_number)) throw InvalidSlotException(slot_number); slot_number = get_internal_slot_number_for_totp(slot_number); - auto gt = get_payload<GetTOTP>(); - gt.slot_number = slot_number; - gt.challenge = challenge; - gt.last_interval = last_interval; - gt.last_totp_time = last_totp_time; - if(user_temporary_password != nullptr && strlen(user_temporary_password)!=0){ //FIXME use string instead of strlen - authorize_packet<GetTOTP, UserAuthorize>(gt, user_temporary_password, device); + if (is_authorization_command_supported()){ + auto gt = get_payload<GetTOTP>(); + gt.slot_number = slot_number; + gt.challenge = challenge; + gt.last_interval = last_interval; + gt.last_totp_time = last_totp_time; + + if(user_temporary_password != nullptr && strlen(user_temporary_password)!=0){ //FIXME use string instead of strlen + authorize_packet<GetTOTP, UserAuthorize>(gt, user_temporary_password, device); + } + auto resp = GetTOTP::CommandTransaction::run(*device, gt); + return resp.data().code; + } else { + auto gt = get_payload<stick10_08::GetTOTP>(); + strcpyT(gt.temporary_user_password, user_temporary_password); + gt.slot_number = slot_number; + auto resp = stick10_08::GetTOTP::CommandTransaction::run(*device, gt); + return resp.data().code; } - auto resp = GetTOTP::CommandTransaction::run(*device, gt); - return resp.data().code; + } bool NitrokeyManager::erase_slot(uint8_t slot_number, const char *temporary_password) { + if (is_authorization_command_supported()){ auto p = get_payload<EraseSlot>(); p.slot_number = slot_number; - authorize_packet<EraseSlot, Authorize>(p, temporary_password, device); - auto resp = EraseSlot::CommandTransaction::run(*device,p); + } else { + auto p = get_payload<stick10_08::EraseSlot>(); + p.slot_number = slot_number; + strcpyT(p.temporary_admin_password, temporary_password); + auto resp = stick10_08::EraseSlot::CommandTransaction::run(*device,p); + } return true; } @@ -171,6 +200,16 @@ namespace nitrokey{ } template <typename T, typename U> + void vector_copy_ranged(T& dest, std::vector<U> &vec, size_t begin, size_t elements_to_copy){ + const size_t d_size = sizeof(dest); + if(d_size < elements_to_copy){ + throw TargetBufferSmallerThanSource(elements_to_copy, d_size); + } + std::fill(dest, dest+d_size, 0); + std::copy(vec.begin() + begin, vec.begin() +begin + elements_to_copy, dest); + } + + template <typename T, typename U> void vector_copy(T& dest, std::vector<U> &vec){ const size_t d_size = sizeof(dest); if(d_size < vec.size()){ @@ -185,60 +224,127 @@ namespace nitrokey{ const char *temporary_password) { if (!is_valid_hotp_slot_number(slot_number)) throw InvalidSlotException(slot_number); - slot_number = get_internal_slot_number_for_hotp(slot_number); - auto payload = get_payload<WriteToHOTPSlot>(); - payload.slot_number = slot_number; - auto secret_bin = misc::hex_string_to_byte(secret); - vector_copy(payload.slot_secret, secret_bin); - strcpyT(payload.slot_name, slot_name); - strcpyT(payload.slot_token_id, token_ID); + int internal_slot_number = get_internal_slot_number_for_hotp(slot_number); + if (is_authorization_command_supported()){ + write_HOTP_slot_authorize(internal_slot_number, slot_name, secret, hotp_counter, use_8_digits, use_enter, use_tokenID, + token_ID, temporary_password); + } else { + write_OTP_slot_no_authorize(internal_slot_number, slot_name, secret, hotp_counter, use_8_digits, use_enter, use_tokenID, + token_ID, temporary_password); + } + return true; + } + + void NitrokeyManager::write_HOTP_slot_authorize(uint8_t slot_number, const char *slot_name, const char *secret, + uint64_t hotp_counter, bool use_8_digits, bool use_enter, + bool use_tokenID, const char *token_ID, const char *temporary_password) { + auto payload = get_payload<WriteToHOTPSlot>(); + payload.slot_number = slot_number; + auto secret_bin = misc::hex_string_to_byte(secret); + vector_copy(payload.slot_secret, secret_bin); + strcpyT(payload.slot_name, slot_name); + strcpyT(payload.slot_token_id, token_ID); switch (device->get_device_model() ){ case DeviceModel::PRO: { payload.slot_counter = hotp_counter; break; } case DeviceModel::STORAGE: { - std::string counter = std::to_string(hotp_counter); + string counter = to_string(hotp_counter); strcpyT(payload.slot_counter_s, counter.c_str()); break; } default: - nitrokey::log::Log::instance()( std::string(__FILE__) + std::to_string(__LINE__) + - std::string(__FUNCTION__) + std::string(" Unhandled device model for HOTP") - , nitrokey::log::Loglevel::DEBUG); + Log::instance()(string(__FILE__) + to_string(__LINE__) + + string(__FUNCTION__) + string(" Unhandled device model for HOTP") + , Loglevel::DEBUG); break; } - payload.use_8_digits = use_8_digits; - payload.use_enter = use_enter; - payload.use_tokenID = use_tokenID; + payload.use_8_digits = use_8_digits; + payload.use_enter = use_enter; + payload.use_tokenID = use_tokenID; - authorize_packet<WriteToHOTPSlot, Authorize>(payload, temporary_password, device); + authorize_packet<WriteToHOTPSlot, Authorize>(payload, temporary_password, device); - auto resp = WriteToHOTPSlot::CommandTransaction::run(*device, payload); - return true; + auto resp = WriteToHOTPSlot::CommandTransaction::run(*device, payload); } bool NitrokeyManager::write_TOTP_slot(uint8_t slot_number, const char *slot_name, const char *secret, uint16_t time_window, bool use_8_digits, bool use_enter, bool use_tokenID, const char *token_ID, const char *temporary_password) { - auto payload = get_payload<WriteToTOTPSlot>(); if (!is_valid_totp_slot_number(slot_number)) throw InvalidSlotException(slot_number); + int internal_slot_number = get_internal_slot_number_for_totp(slot_number); + + if (is_authorization_command_supported()){ + write_TOTP_slot_authorize(internal_slot_number, slot_name, secret, time_window, use_8_digits, use_enter, use_tokenID, + token_ID, temporary_password); + } else { + write_OTP_slot_no_authorize(internal_slot_number, slot_name, secret, time_window, use_8_digits, use_enter, use_tokenID, + token_ID, temporary_password); + } - slot_number = get_internal_slot_number_for_totp(slot_number); - payload.slot_number = slot_number; - auto secret_bin = misc::hex_string_to_byte(secret); - vector_copy(payload.slot_secret, secret_bin); - strcpyT(payload.slot_name, slot_name); - strcpyT(payload.slot_token_id, token_ID); - payload.slot_interval = time_window; //FIXME naming - payload.use_8_digits = use_8_digits; - payload.use_enter = use_enter; - payload.use_tokenID = use_tokenID; - - authorize_packet<WriteToTOTPSlot, Authorize>(payload, temporary_password, device); - - auto resp = WriteToTOTPSlot::CommandTransaction::run(*device, payload); - return true; + return true; + } + + void NitrokeyManager::write_OTP_slot_no_authorize(uint8_t internal_slot_number, const char *slot_name, + const char *secret, + uint64_t counter_or_interval, bool use_8_digits, bool use_enter, + bool use_tokenID, const char *token_ID, + const char *temporary_password) const { + + auto payload2 = get_payload<stick10_08::SendOTPData>(); + strcpyT(payload2.temporary_admin_password, temporary_password); + strcpyT(payload2.data, slot_name); + payload2.setTypeName(); + stick10_08::SendOTPData::CommandTransaction::run(*device, payload2); + + payload2.setTypeSecret(); + payload2.id = 0; + auto secret_bin = misc::hex_string_to_byte(secret); + auto remaining_secret_length = secret_bin.size(); + const auto maximum_OTP_secret_size = 40; + if(remaining_secret_length > maximum_OTP_secret_size){ + throw TargetBufferSmallerThanSource(remaining_secret_length, maximum_OTP_secret_size); + } + + while (remaining_secret_length>0){ + const auto bytesToCopy = std::min(sizeof(payload2.data), remaining_secret_length); + const auto start = secret_bin.size() - remaining_secret_length; + memset(payload2.data, 0, sizeof(payload2.data)); + vector_copy_ranged(payload2.data, secret_bin, start, bytesToCopy); + stick10_08::SendOTPData::CommandTransaction::run(*device, payload2); + remaining_secret_length -= bytesToCopy; + payload2.id++; + } + + auto payload = get_payload<stick10_08::WriteToOTPSlot>(); + strcpyT(payload.temporary_admin_password, temporary_password); + strcpyT(payload.slot_token_id, token_ID); + payload.use_8_digits = use_8_digits; + payload.use_enter = use_enter; + payload.use_tokenID = use_tokenID; + payload.slot_counter_or_interval = counter_or_interval; + payload.slot_number = internal_slot_number; + stick10_08::WriteToOTPSlot::CommandTransaction::run(*device, payload); + } + + void NitrokeyManager::write_TOTP_slot_authorize(uint8_t slot_number, const char *slot_name, const char *secret, + uint16_t time_window, bool use_8_digits, bool use_enter, + bool use_tokenID, const char *token_ID, const char *temporary_password) { + auto payload = get_payload<WriteToTOTPSlot>(); + payload.slot_number = slot_number; + auto secret_bin = misc::hex_string_to_byte(secret); + vector_copy(payload.slot_secret, secret_bin); + strcpyT(payload.slot_name, slot_name); + strcpyT(payload.slot_token_id, token_ID); + payload.slot_interval = time_window; //FIXME naming + payload.use_8_digits = use_8_digits; + payload.use_enter = use_enter; + payload.use_tokenID = use_tokenID; + + authorize_packet<WriteToTOTPSlot, Authorize>(payload, temporary_password, device); + + auto resp = WriteToTOTPSlot::CommandTransaction::run(*device, payload); } const char * NitrokeyManager::get_totp_slot_name(uint8_t slot_number) { @@ -461,16 +567,18 @@ namespace nitrokey{ void NitrokeyManager::write_config(uint8_t numlock, uint8_t capslock, uint8_t scrolllock, bool enable_user_password, bool delete_user_password, const char *admin_temporary_password) { - auto p = get_payload<WriteGeneralConfig>(); + auto p = get_payload<stick10_08::WriteGeneralConfig>(); p.numlock = (uint8_t) numlock; p.capslock = (uint8_t) capslock; p.scrolllock = (uint8_t) scrolllock; p.enable_user_password = (uint8_t) enable_user_password; p.delete_user_password = (uint8_t) delete_user_password; - - authorize_packet<WriteGeneralConfig, Authorize>(p, admin_temporary_password, device); - - WriteGeneralConfig::CommandTransaction::run(*device, p); + if (is_authorization_command_supported()){ + authorize_packet<stick10_08::WriteGeneralConfig, Authorize>(p, admin_temporary_password, device); + } else { + strcpyT(p.temporary_admin_password, admin_temporary_password); + } + stick10_08::WriteGeneralConfig::CommandTransaction::run(*device, p); } vector<uint8_t> NitrokeyManager::read_config() { @@ -480,6 +588,29 @@ namespace nitrokey{ return v; } + bool NitrokeyManager::is_authorization_command_supported(){ + //authorization command is supported for versions equal or below: + auto m = std::unordered_map<DeviceModel , int, EnumClassHash>({ + {DeviceModel::PRO, 7}, + {DeviceModel::STORAGE, 43}, + }); + return get_major_firmware_version() <= m[device->get_device_model()]; + } + + int NitrokeyManager::get_major_firmware_version(){ + switch(device->get_device_model()){ + case DeviceModel::PRO:{ + auto status_p = GetStatus::CommandTransaction::run(*device); + return status_p.data().firmware_version; //7 or 8 + } + case DeviceModel::STORAGE:{ + auto status = stick20::GetDeviceStatus::CommandTransaction::run(*device); + return status.data().versionInfo.major; + } + } + return 0; + } + bool NitrokeyManager::is_AES_supported(const char *user_password) { auto a = get_payload<IsAESSupported>(); strcpyT(a.user_password, user_password); diff --git a/command_id.cc b/command_id.cc index 9512b7d..f76a358 100644 --- a/command_id.cc +++ b/command_id.cc @@ -139,6 +139,12 @@ const char *commandid_to_string(CommandID id) { return "DETECT_SC_AES"; case CommandID::NEW_AES_KEY: return "NEW_AES_KEY"; + case CommandID::WRITE_TO_SLOT_2: + return "WRITE_TO_SLOT_2"; + break; + case CommandID::SEND_OTP_DATA: + return "SEND_OTP_DATA"; + break; } return "UNKNOWN"; } diff --git a/include/NitrokeyManager.h b/include/NitrokeyManager.h index 60fa753..fd39445 100644 --- a/include/NitrokeyManager.h +++ b/include/NitrokeyManager.h @@ -5,6 +5,7 @@ #include "log.h" #include "device_proto.h" #include "stick10_commands.h" +#include "stick10_commands_0.8.h" #include "stick20_commands.h" #include <vector> #include <memory> @@ -110,6 +111,12 @@ namespace nitrokey { int get_progress_bar_value(); ~NitrokeyManager(); + bool is_authorization_command_supported(); + + template <typename S, typename A, typename T> + void authorize_packet(T &package, const char *admin_temporary_password, shared_ptr<Device> device); + int get_major_firmware_version(); + private: NitrokeyManager(); @@ -128,6 +135,19 @@ namespace nitrokey { template <typename ProCommand, PasswordKind StoKind> void change_PIN_general(char *current_PIN, char *new_PIN); + void write_HOTP_slot_authorize(uint8_t slot_number, const char *slot_name, const char *secret, uint64_t hotp_counter, + bool use_8_digits, bool use_enter, bool use_tokenID, const char *token_ID, + const char *temporary_password); + + void write_TOTP_slot_authorize(uint8_t slot_number, const char *slot_name, const char *secret, uint16_t time_window, + bool use_8_digits, bool use_enter, bool use_tokenID, const char *token_ID, + const char *temporary_password); + + void write_OTP_slot_no_authorize(uint8_t internal_slot_number, const char *slot_name, const char *secret, + uint64_t counter_or_interval, + bool use_8_digits, bool use_enter, bool use_tokenID, const char *token_ID, + const char *temporary_password) const; + }; } diff --git a/include/command_id.h b/include/command_id.h index a3806f0..346b750 100644 --- a/include/command_id.h +++ b/include/command_id.h @@ -65,6 +65,8 @@ enum class CommandID : uint8_t { FACTORY_RESET = 0x13, CHANGE_USER_PIN = 0x14, CHANGE_ADMIN_PIN = 0x15, + WRITE_TO_SLOT_2 = 0x16, + SEND_OTP_DATA = 0x17, ENABLE_CRYPTED_PARI = 0x20, DISABLE_CRYPTED_PARI = 0x20 + 1, //@unused diff --git a/include/device.h b/include/device.h index 3f18921..62c4073 100644 --- a/include/device.h +++ b/include/device.h @@ -12,6 +12,15 @@ namespace nitrokey { namespace device { using namespace std::chrono_literals; + struct EnumClassHash + { + template <typename T> + std::size_t operator()(T t) const + { + return static_cast<std::size_t>(t); + } + }; + enum class DeviceModel{ PRO, STORAGE diff --git a/include/dissect.h b/include/dissect.h index 59e6e9c..8c975c5 100644 --- a/include/dissect.h +++ b/include/dissect.h @@ -36,44 +36,65 @@ class QueryDissector : semantics::non_constructible { } }; + + + template <CommandID id, class HIDPacket> class ResponseDissector : semantics::non_constructible { public: + static std::string status_translate_device(int status){ + auto enum_status = static_cast<proto::stick10::device_status>(status); + switch (enum_status){ + case stick10::device_status::ok: return "OK"; + case stick10::device_status::busy: return "BUSY"; + case stick10::device_status::error: return "ERROR"; + case stick10::device_status::received_report: return "RECEIVED_REPORT"; + } + return std::string("UNKNOWN: ") + std::to_string(status); + } + + static std::string to_upper(std::string str){ + for (auto & c: str) c = toupper(c); + return str; + } + static std::string status_translate_command(int status){ + auto enum_status = static_cast<proto::stick10::command_status >(status); + switch (enum_status) { +#define p(X) case X: return to_upper(std::string(#X)); + p(stick10::command_status::ok) + p(stick10::command_status::wrong_CRC) + p(stick10::command_status::wrong_slot) + p(stick10::command_status::slot_not_programmed) + p(stick10::command_status::wrong_password) + p(stick10::command_status::not_authorized) + p(stick10::command_status::timestamp_warning) + p(stick10::command_status::no_name_error) + p(stick10::command_status::not_supported) + p(stick10::command_status::unknown_command) + p(stick10::command_status::AES_dec_failed) +#undef p + } + return std::string("UNKNOWN: ") + std::to_string(status); + } + static std::string dissect(const HIDPacket &pod) { std::stringstream out; // FIXME use values from firmware (possibly generate separate // header automatically) - std::string status[4]; - status[0] = " STATUS_READY"; - status[1] = " STATUS_BUSY"; - status[2] = " STATUS_ERROR"; - status[3] = " STATUS_RECEIVED_REPORT"; - std::string cmd[11]; - cmd[0] = " CMD_STATUS_OK"; - cmd[1] = " CMD_STATUS_WRONG_CRC"; - cmd[2] = " CMD_STATUS_WRONG_SLOT"; - cmd[3] = " CMD_STATUS_SLOT_NOT_PROGRAMMED"; - cmd[4] = " CMD_STATUS_WRONG_PASSWORD"; - cmd[5] = " CMD_STATUS_NOT_AUTHORIZED"; - cmd[6] = " CMD_STATUS_TIMESTAMP_WARNING"; - cmd[7] = " CMD_STATUS_NO_NAME_ERROR"; - cmd[8] = " CMD_STATUS_NOT_SUPPORTED"; - cmd[9] = " CMD_STATUS_UNKNOWN_COMMAND"; - cmd[10] = " CMD_STATUS_AES_DEC_FAILED"; out << "Raw HID packet:" << std::endl; out << ::nitrokey::misc::hexdump((const char *)(&pod), sizeof pod); out << "Device status:\t" << pod.device_status + 0 << " " - << status[pod.device_status] << std::endl; + << status_translate_device(pod.device_status) << std::endl; out << "Command ID:\t" << commandid_to_string((CommandID)(pod.command_id)) << " hex: " << std::hex << (int)pod.command_id << std::endl; out << "Last command CRC:\t" << std::hex << std::setw(2) << std::setfill('0') << pod.last_command_crc << std::endl; out << "Last command status:\t" << pod.last_command_status + 0 << " " - << cmd[pod.last_command_status] << std::endl; + << status_translate_command(pod.last_command_status) << std::endl; out << "CRC:\t" << std::hex << std::setw(2) << std::setfill('0') << pod.crc << std::endl; diff --git a/include/stick10_commands_0.8.h b/include/stick10_commands_0.8.h new file mode 100644 index 0000000..9594d1e --- /dev/null +++ b/include/stick10_commands_0.8.h @@ -0,0 +1,312 @@ +// +// Created by sz on 08.11.16. +// + +#ifndef LIBNITROKEY_STICK10_COMMANDS_0_8_H +#define LIBNITROKEY_STICK10_COMMANDS_0_8_H + +#include <bitset> +#include <iomanip> +#include <string> +#include <sstream> +#include "inttypes.h" +#include "command.h" +#include "device_proto.h" +#include "stick10_commands.h" + +namespace nitrokey { + namespace proto { + +/* + * Stick10 protocol definition + */ + namespace stick10_08 { + using stick10::FirstAuthenticate; + using stick10::UserAuthenticate; + using stick10::SetTime; + using stick10::GetStatus; + using stick10::BuildAESKey; + using stick10::ChangeAdminPin; + using stick10::ChangeUserPin; + using stick10::EnablePasswordSafe; + using stick10::ErasePasswordSafeSlot; + using stick10::FactoryReset; + using stick10::GetPasswordRetryCount; + using stick10::GetUserPasswordRetryCount; + using stick10::GetPasswordSafeSlotLogin; + using stick10::GetPasswordSafeSlotName; + using stick10::GetPasswordSafeSlotPassword; + using stick10::GetPasswordSafeSlotStatus; + using stick10::GetSlotName; + using stick10::IsAESSupported; + using stick10::LockDevice; + using stick10::PasswordSafeInitKey; + using stick10::PasswordSafeSendSlotViaHID; + using stick10::SetPasswordSafeSlotData; + using stick10::SetPasswordSafeSlotData2; + using stick10::UnlockUserPassword; + using stick10::ReadSlot; + + class EraseSlot : Command<CommandID::ERASE_SLOT> { + public: + struct CommandPayload { + uint8_t slot_number; + uint8_t temporary_admin_password[25]; + + bool isValid() const { return !(slot_number & 0xF0); } + std::string dissect() const { + std::stringstream ss; + ss << "slot_number:\t" << (int)(slot_number) << std::endl; + return ss.str(); + } + } __packed; + + typedef Transaction<command_id(), struct CommandPayload, struct EmptyPayload> + CommandTransaction; + }; + + class SendOTPData : Command<CommandID::SEND_OTP_DATA> { + //admin auth + public: + struct CommandPayload { + uint8_t temporary_admin_password[25]; + uint8_t type; //S-secret, N-name + uint8_t id; //multiple reports for values longer than 30 bytes + uint8_t data[30]; //data, does not need null termination + + bool isValid() const { return true; } + + void setTypeName(){ + type = 'N'; + } + void setTypeSecret(){ + type = 'S'; + } + + std::string dissect() const { + std::stringstream ss; + ss << "temporary_admin_password:\t" << temporary_admin_password << std::endl; + ss << "type:\t" << type << std::endl; + ss << "id:\t" << (int)id << std::endl; + ss << "data:" << std::endl + << ::nitrokey::misc::hexdump((const char *) (&data), sizeof data); + return ss.str(); + } + } __packed; + + + struct ResponsePayload { + union { + uint8_t data[40]; + } __packed; + + bool isValid() const { return true; } + std::string dissect() const { + std::stringstream ss; + ss << "data:" << std::endl + << ::nitrokey::misc::hexdump((const char *) (&data), sizeof data); + return ss.str(); + } + } __packed; + + + typedef Transaction<command_id(), struct CommandPayload, struct ResponsePayload> + CommandTransaction; + }; + + class WriteToOTPSlot : Command<CommandID::WRITE_TO_SLOT> { + //admin auth + public: + struct CommandPayload { + uint8_t temporary_admin_password[25]; + uint8_t slot_number; + union { + uint64_t slot_counter_or_interval; + uint8_t slot_counter_s[8]; + } __packed; + union { + uint8_t _slot_config; + struct { + bool use_8_digits : 1; + bool use_enter : 1; + bool use_tokenID : 1; + }; + }; + union { + uint8_t slot_token_id[13]; /** OATH Token Identifier */ + struct { /** @see https://openauthentication.org/token-specs/ */ + uint8_t omp[2]; + uint8_t tt[2]; + uint8_t mui[8]; + uint8_t keyboard_layout; //disabled feature in nitroapp as of 20160805 + } slot_token_fields; + }; + + bool isValid() const { return true; } + + std::string dissect() const { + std::stringstream ss; + ss << "temporary_admin_password:\t" << temporary_admin_password << std::endl; + ss << "slot_config:\t" << std::bitset<8>((int) _slot_config) << std::endl; + ss << "\tuse_8_digits(0):\t" << use_8_digits << std::endl; + ss << "\tuse_enter(1):\t" << use_enter << std::endl; + ss << "\tuse_tokenID(2):\t" << use_tokenID << std::endl; + ss << "slot_number:\t" << (int) (slot_number) << std::endl; + ss << "slot_counter_or_interval:\t[" << (int) slot_counter_or_interval << "]\t" + << ::nitrokey::misc::hexdump((const char *) (&slot_counter_or_interval), sizeof slot_counter_or_interval, false); + + ss << "slot_token_id:\t"; + for (auto i : slot_token_id) + ss << std::hex << std::setw(2) << std::setfill('0') << (int) i << " "; + ss << std::endl; + + return ss.str(); + } + } __packed; + + typedef Transaction<command_id(), struct CommandPayload, struct EmptyPayload> + CommandTransaction; + }; + + class GetHOTP : Command<CommandID::GET_CODE> { + public: + struct CommandPayload { + uint8_t slot_number; + struct { + uint64_t challenge; //@unused + uint64_t last_totp_time; //@unused + uint8_t last_interval; //@unused + } __packed _unused; + uint8_t temporary_user_password[25]; + + bool isValid() const { return (slot_number & 0xF0); } + std::string dissect() const { + std::stringstream ss; + ss << "temporary_user_password:\t" << temporary_user_password << std::endl; + ss << "slot_number:\t" << (int)(slot_number) << std::endl; + return ss.str(); + } + } __packed; + + struct ResponsePayload { + union { + uint8_t whole_response[18]; //14 bytes reserved for config, but used only 1 + struct { + uint32_t code; + union{ + uint8_t _slot_config; + struct{ + bool use_8_digits : 1; + bool use_enter : 1; + bool use_tokenID : 1; + }; + }; + } __packed; + } __packed; + + bool isValid() const { return true; } + std::string dissect() const { + std::stringstream ss; + ss << "code:\t" << (code) << std::endl; + ss << "slot_config:\t" << std::bitset<8>((int)_slot_config) << std::endl; + ss << "\tuse_8_digits(0):\t" << use_8_digits << std::endl; + ss << "\tuse_enter(1):\t" << use_enter << std::endl; + ss << "\tuse_tokenID(2):\t" << use_tokenID << std::endl; + return ss.str(); + } + } __packed; + + typedef Transaction<command_id(), struct CommandPayload, struct ResponsePayload> + CommandTransaction; + }; + + + class GetTOTP : Command<CommandID::GET_CODE> { + //user auth + public: + struct CommandPayload { + uint8_t slot_number; + uint64_t challenge; //@unused + uint64_t last_totp_time; //@unused + uint8_t last_interval; //@unused + uint8_t temporary_user_password[25]; + + bool isValid() const { return !(slot_number & 0xF0); } + std::string dissect() const { + std::stringstream ss; + ss << "temporary_user_password:\t" << temporary_user_password << std::endl; + ss << "slot_number:\t" << (int)(slot_number) << std::endl; + ss << "challenge:\t" << (challenge) << std::endl; + ss << "last_totp_time:\t" << (last_totp_time) << std::endl; + ss << "last_interval:\t" << (int)(last_interval) << std::endl; + return ss.str(); + } + } __packed; + + struct ResponsePayload { + union { + uint8_t whole_response[18]; //14 bytes reserved for config, but used only 1 + struct { + uint32_t code; + union{ + uint8_t _slot_config; + struct{ + bool use_8_digits : 1; + bool use_enter : 1; + bool use_tokenID : 1; + }; + }; + } __packed ; + } __packed ; + + bool isValid() const { return true; } + std::string dissect() const { + std::stringstream ss; + ss << "code:\t" << (code) << std::endl; + ss << "slot_config:\t" << std::bitset<8>((int)_slot_config) << std::endl; + ss << "\tuse_8_digits(0):\t" << use_8_digits << std::endl; + ss << "\tuse_enter(1):\t" << use_enter << std::endl; + ss << "\tuse_tokenID(2):\t" << use_tokenID << std::endl; + return ss.str(); + } + } __packed; + + typedef Transaction<command_id(), struct CommandPayload, struct ResponsePayload> + CommandTransaction; + }; + + + class WriteGeneralConfig : Command<CommandID::WRITE_CONFIG> { + //admin auth + public: + struct CommandPayload { + union{ + uint8_t config[5]; + struct{ + uint8_t numlock; /** 0-1: HOTP slot number from which the code will be get on double press, other value - function disabled */ + uint8_t capslock; /** same as numlock */ + uint8_t scrolllock; /** same as numlock */ + uint8_t enable_user_password; + uint8_t delete_user_password; + }; + }; + uint8_t temporary_admin_password[25]; + + std::string dissect() const { + std::stringstream ss; + ss << "numlock:\t" << (int)numlock << std::endl; + ss << "capslock:\t" << (int)capslock << std::endl; + ss << "scrolllock:\t" << (int)scrolllock << std::endl; + ss << "enable_user_password:\t" << (bool) enable_user_password << std::endl; + ss << "delete_user_password:\t" << (bool) delete_user_password << std::endl; + return ss.str(); + } + } __packed; + + typedef Transaction<command_id(), struct CommandPayload, struct EmptyPayload> + CommandTransaction; + }; + } + } +} +#endif //LIBNITROKEY_STICK10_COMMANDS_0_8_H diff --git a/include/stick20_commands.h b/include/stick20_commands.h index 1af9da3..e6df770 100644 --- a/include/stick20_commands.h +++ b/include/stick20_commands.h @@ -125,7 +125,17 @@ namespace nitrokey { uint16_t MagicNumber_StickConfig_u16; uint8_t ReadWriteFlagUncryptedVolume_u8; uint8_t ReadWriteFlagCryptedVolume_u8; + + union{ uint8_t VersionInfo_au8[4]; + struct { + uint8_t __unused; + uint8_t major; + uint8_t __unused2; + uint8_t minor; + } __packed versionInfo; + }; + uint8_t ReadWriteFlagHiddenVolume_u8; uint8_t FirmwareLocked_u8; uint8_t NewSDCardFound_u8; @@ -13,10 +13,11 @@ std::vector<uint8_t> hex_string_to_byte(const char* hexString){ const size_t big_string_size = 256; //arbitrary 'big' number const size_t s_size = strlen(hexString); const size_t d_size = s_size/2; - if (s_size%2!=0 || s_size==0 || s_size>big_string_size){ + if (s_size%2!=0 || s_size>big_string_size){ throw InvalidHexString(0); } - auto data = std::vector<uint8_t>(d_size, 0); + auto data = std::vector<uint8_t>(); + data.reserve(d_size); char buf[2]; for(int i=0; i<s_size; i++){ @@ -28,7 +29,7 @@ std::vector<uint8_t> hex_string_to_byte(const char* hexString){ } buf[i%2] = c; if (i%2==1){ - data[i/2] = strtoul(buf, NULL, 16) & 0xFF; + data.push_back( strtoul(buf, NULL, 16) & 0xFF ); } } return data; diff --git a/unittest/conftest.py b/unittest/conftest.py index 68227d5..88bf7d0 100644 --- a/unittest/conftest.py +++ b/unittest/conftest.py @@ -2,6 +2,16 @@ import pytest from misc import ffi +device_type = None + +def skip_if_device_version_lower_than(allowed_devices): + global device_type + model, version = device_type + infinite_version_number = 999 + if allowed_devices.get(model, infinite_version_number) > version: + pytest.skip('This device model is not applicable to run this test') + + @pytest.fixture(scope="module") def C(request): fp = '../NK_C_API.h' @@ -24,7 +34,11 @@ def C(request): nk_login = C.NK_login_auto() if nk_login != 1: print('No devices detected!') - assert nk_login == 1 # returns 0 if not connected or wrong model or 1 when connected + assert nk_login != 0 # returns 0 if not connected or wrong model or 1 when connected + global device_type + firmware_version = C.NK_get_major_firmware_version() + model = 'P' if firmware_version in [7,8] else 'S' + device_type = (model, firmware_version) # assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK # assert C.NK_user_authenticate(DefaultPasswords.USER, DefaultPasswords.USER_TEMP) == DeviceErrorCode.STATUS_OK diff --git a/unittest/constants.py b/unittest/constants.py index 78a219b..0897b42 100644 --- a/unittest/constants.py +++ b/unittest/constants.py @@ -2,7 +2,7 @@ from enum import Enum from misc import to_hex RFC_SECRET_HR = '12345678901234567890' -RFC_SECRET = to_hex(RFC_SECRET_HR) # '12345678901234567890' +RFC_SECRET = to_hex(RFC_SECRET_HR) # '31323334353637383930...' # print( repr((RFC_SECRET, RFC_SECRET_, len(RFC_SECRET))) ) diff --git a/unittest/misc.py b/unittest/misc.py index b45436d..f4d7731 100644 --- a/unittest/misc.py +++ b/unittest/misc.py @@ -20,21 +20,29 @@ def cast_pointer_to_tuple(obj, typen, len): # config = cast_pointer_to_tuple(config_raw_data, 'uint8_t', 5) return tuple(ffi.cast("%s [%d]" % (typen, len), obj)[0:len]) -def get_firmware_version_from_status(C): - status = gs(C.NK_status()) - status = [s if 'firmware_version' in s else '' for s in status.split('\n')] - firmware = status[0].split(':')[1] + +def get_devices_firmware_version(C): + firmware = C.NK_get_major_firmware_version() return firmware def is_pro_rtm_07(C): - firmware = get_firmware_version_from_status(C) - return '07 00' in firmware + firmware = get_devices_firmware_version(C) + return firmware == 7 + + +def is_pro_rtm_08(C): + firmware = get_devices_firmware_version(C) + return firmware == 8 def is_storage(C): """ exact firmware storage is sent by other function """ - firmware = get_firmware_version_from_status(C) - return '01 00' in firmware
\ No newline at end of file + # TODO identify connected device directly + return not is_pro_rtm_08(C) and not is_pro_rtm_07(C) + + +def is_long_OTP_secret_handled(C): + return is_pro_rtm_08(C) or is_storage(C) and get_devices_firmware_version(C) > 43 diff --git a/unittest/requirements.txt b/unittest/requirements.txt new file mode 100644 index 0000000..7224741 --- /dev/null +++ b/unittest/requirements.txt @@ -0,0 +1,4 @@ +cffi +pytest-repeat +pytest-randomly +enum diff --git a/unittest/test3.cc b/unittest/test3.cc new file mode 100644 index 0000000..9049365 --- /dev/null +++ b/unittest/test3.cc @@ -0,0 +1,220 @@ +// +// Created by sz on 08.11.16. +// + +#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() + +static const char *const default_admin_pin = "12345678"; +static const char *const default_user_pin = "123456"; +const char * temporary_password = "123456789012345678901234"; +const char * RFC_SECRET = "12345678901234567890"; + +#include "catch.hpp" + +#include <iostream> +#include <string.h> +#include <NitrokeyManager.h> +#include "device_proto.h" +#include "log.h" +#include "stick10_commands_0.8.h" +//#include "stick20_commands.h" + +using namespace std; +using namespace nitrokey::device; +using namespace nitrokey::proto; +using namespace nitrokey::proto::stick10_08; +using namespace nitrokey::log; +using namespace nitrokey::misc; + +void connect_and_setup(Stick10 &stick) { + bool connected = stick.connect(); + REQUIRE(connected == true); + Log::instance().set_loglevel(Loglevel::DEBUG); +} + +void authorize(Stick10 &stick) { + auto authreq = get_payload<FirstAuthenticate>(); + strcpy((char *) (authreq.card_password), default_admin_pin); + strcpy((char *) (authreq.temporary_password), temporary_password); + FirstAuthenticate::CommandTransaction::run(stick, authreq); + + auto user_auth = get_payload<UserAuthenticate>(); + strcpyT(user_auth.temporary_password, temporary_password); + strcpyT(user_auth.card_password, default_user_pin); + UserAuthenticate::CommandTransaction::run(stick, user_auth); +} + +TEST_CASE("write slot", "[pronew]"){ + Stick10 stick; + connect_and_setup(stick); + authorize(stick); + + auto p2 = get_payload<SendOTPData>(); + strcpyT(p2.temporary_admin_password, temporary_password); + p2.setTypeName(); + strcpyT(p2.data, "test name aaa"); + stick10_08::SendOTPData::CommandTransaction::run(stick, p2); + + p2 = get_payload<SendOTPData>(); + strcpyT(p2.temporary_admin_password, temporary_password); + strcpyT(p2.data, RFC_SECRET); + p2.setTypeSecret(); + stick10_08::SendOTPData::CommandTransaction::run(stick, p2); + + auto p = get_payload<WriteToOTPSlot>(); + strcpyT(p.temporary_admin_password, temporary_password); + p.use_8_digits = true; + p.slot_number = 0 + 0x10; + p.slot_counter_or_interval = 0; + stick10_08::WriteToOTPSlot::CommandTransaction::run(stick, p); + + auto pc = get_payload<WriteGeneralConfig>(); + pc.enable_user_password = 0; + strcpyT(pc.temporary_admin_password, temporary_password); + WriteGeneralConfig::CommandTransaction::run(stick, pc); + + auto p3 = get_payload<GetHOTP>(); + p3.slot_number = 0 + 0x10; + GetHOTP::CommandTransaction::run(stick, p3); + +} + + +TEST_CASE("erase slot", "[pronew]"){ + Stick10 stick; + connect_and_setup(stick); + authorize(stick); + + auto p = get_payload<WriteGeneralConfig>(); + p.enable_user_password = 0; + strcpyT(p.temporary_admin_password, temporary_password); + WriteGeneralConfig::CommandTransaction::run(stick, p); + + auto p3 = get_payload<GetHOTP>(); + p3.slot_number = 0 + 0x10; + GetHOTP::CommandTransaction::run(stick, p3); + + auto erase_payload = get_payload<EraseSlot>(); + erase_payload.slot_number = 0 + 0x10; + strcpyT(erase_payload.temporary_admin_password, temporary_password); + EraseSlot::CommandTransaction::run(stick, erase_payload); + + auto p4 = get_payload<GetHOTP>(); + p4.slot_number = 0 + 0x10; + REQUIRE_THROWS( + GetHOTP::CommandTransaction::run(stick, p4) + ); +} + +TEST_CASE("write general config", "[pronew]") { + Stick10 stick; + connect_and_setup(stick); + authorize(stick); + + auto p = get_payload<WriteGeneralConfig>(); + p.enable_user_password = 1; + REQUIRE_THROWS( + WriteGeneralConfig::CommandTransaction::run(stick, p); + ); + strcpyT(p.temporary_admin_password, temporary_password); + WriteGeneralConfig::CommandTransaction::run(stick, p); +} + +TEST_CASE("authorize user HOTP", "[pronew]") { + Stick10 stick; + connect_and_setup(stick); + authorize(stick); + + { + auto p = get_payload<WriteGeneralConfig>(); + p.enable_user_password = 1; + strcpyT(p.temporary_admin_password, temporary_password); + WriteGeneralConfig::CommandTransaction::run(stick, p); + } + + auto p2 = get_payload<SendOTPData>(); + strcpyT(p2.temporary_admin_password, temporary_password); + p2.setTypeName(); + strcpyT(p2.data, "test name aaa"); + stick10_08::SendOTPData::CommandTransaction::run(stick, p2); + + p2 = get_payload<SendOTPData>(); + strcpyT(p2.temporary_admin_password, temporary_password); + strcpyT(p2.data, RFC_SECRET); + p2.setTypeSecret(); + stick10_08::SendOTPData::CommandTransaction::run(stick, p2); + + auto p = get_payload<WriteToOTPSlot>(); + strcpyT(p.temporary_admin_password, temporary_password); + p.use_8_digits = true; + p.slot_number = 0 + 0x10; + p.slot_counter_or_interval = 0; + stick10_08::WriteToOTPSlot::CommandTransaction::run(stick, p); + + + auto p3 = get_payload<GetHOTP>(); + p3.slot_number = 0 + 0x10; + REQUIRE_THROWS( + GetHOTP::CommandTransaction::run(stick, p3); + ); + strcpyT(p3.temporary_user_password, temporary_password); + auto code_response = GetHOTP::CommandTransaction::run(stick, p3); + REQUIRE(code_response.data().code == 84755224); + +} + +TEST_CASE("check firmware version", "[pronew]") { + Stick10 stick; + connect_and_setup(stick); + + auto p = GetStatus::CommandTransaction::run(stick); + REQUIRE(p.data().firmware_version == 8); +} + +TEST_CASE("authorize user TOTP", "[pronew]") { + Stick10 stick; + connect_and_setup(stick); + authorize(stick); + + { + auto p = get_payload<WriteGeneralConfig>(); + p.enable_user_password = 1; + strcpyT(p.temporary_admin_password, temporary_password); + WriteGeneralConfig::CommandTransaction::run(stick, p); + } + + auto p2 = get_payload<SendOTPData>(); + strcpyT(p2.temporary_admin_password, temporary_password); + p2.setTypeName(); + strcpyT(p2.data, "test name TOTP"); + stick10_08::SendOTPData::CommandTransaction::run(stick, p2); + + p2 = get_payload<SendOTPData>(); + strcpyT(p2.temporary_admin_password, temporary_password); + strcpyT(p2.data, RFC_SECRET); + p2.setTypeSecret(); + stick10_08::SendOTPData::CommandTransaction::run(stick, p2); + + auto p = get_payload<WriteToOTPSlot>(); + strcpyT(p.temporary_admin_password, temporary_password); + p.use_8_digits = true; + p.slot_number = 0 + 0x20; + p.slot_counter_or_interval = 30; + stick10_08::WriteToOTPSlot::CommandTransaction::run(stick, p); + + auto p_get_totp = get_payload<GetTOTP>(); + p_get_totp.slot_number = 0 + 0x20; + + REQUIRE_THROWS( + GetTOTP::CommandTransaction::run(stick, p_get_totp); + ); + strcpyT(p_get_totp.temporary_user_password, temporary_password); + + auto p_set_time = get_payload<SetTime>(); + p_set_time.reset = 1; + p_set_time.time = 59; + SetTime::CommandTransaction::run(stick, p_set_time); + auto code = GetTOTP::CommandTransaction::run(stick, p_get_totp); + REQUIRE(code.data().code == 94287082); + +}
\ No newline at end of file diff --git a/unittest/test_library.py b/unittest/test_library.py index d0eef80..b24c72a 100644 --- a/unittest/test_library.py +++ b/unittest/test_library.py @@ -1,8 +1,9 @@ import pytest -from misc import ffi, gs, to_hex +from misc import ffi, gs, to_hex, is_pro_rtm_07, is_long_OTP_secret_handled from constants import DefaultPasswords, DeviceErrorCode, RFC_SECRET, LibraryErrors + def test_too_long_strings(C): new_password = '123123123' long_string = 'a' * 100 @@ -35,30 +36,28 @@ def test_invalid_slot(C): assert gs(C.NK_get_password_safe_slot_login(invalid_slot)) == '' assert C.NK_get_last_command_status() == LibraryErrors.INVALID_SLOT + @pytest.mark.parametrize("invalid_hex_string", - ['text', '00 ', '0xff', 'zzzzzzzzzzzz', 'fff', '', 'f' * 257, 'f' * 258]) + ['text', '00 ', '0xff', 'zzzzzzzzzzzz', 'fff', 'f' * 257, 'f' * 258]) def test_invalid_secret_hex_string_for_OTP_write(C, invalid_hex_string): """ Tests for invalid secret hex string during writing to OTP slot. Invalid strings are not hexadecimal number, empty or longer than 255 characters. """ + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK assert C.NK_write_hotp_slot(1, 'slot_name', invalid_hex_string, 0, True, False, False, '', DefaultPasswords.ADMIN_TEMP) == LibraryErrors.INVALID_HEX_STRING assert C.NK_write_totp_slot(1, 'python_test', invalid_hex_string, 30, True, False, False, "", DefaultPasswords.ADMIN_TEMP) == LibraryErrors.INVALID_HEX_STRING - def test_warning_binary_bigger_than_secret_buffer(C): invalid_hex_string = to_hex('1234567890') * 3 + if is_long_OTP_secret_handled(C): + invalid_hex_string *= 2 assert C.NK_write_hotp_slot(1, 'slot_name', invalid_hex_string, 0, True, False, False, '', DefaultPasswords.ADMIN_TEMP) == LibraryErrors.TARGET_BUFFER_SIZE_SMALLER_THAN_SOURCE -@pytest.mark.xfail(reason="TODO") -def test_OTP_secret_started_from_null(C): - assert False - - @pytest.mark.skip(reason='Experimental') def test_clear(C): d = 'asdasdasd' diff --git a/unittest/test_pro.py b/unittest/test_pro.py index 6ab2af9..4a2a504 100644 --- a/unittest/test_pro.py +++ b/unittest/test_pro.py @@ -1,10 +1,15 @@ import pytest +from conftest import skip_if_device_version_lower_than from constants import DefaultPasswords, DeviceErrorCode, RFC_SECRET from misc import ffi, gs, wait, cast_pointer_to_tuple -from misc import is_pro_rtm_07, is_storage +from misc import is_pro_rtm_07, is_pro_rtm_08, is_storage + def test_enable_password_safe(C): + """ + All Password Safe tests depend on AES keys being initialized. They will fail otherwise. + """ assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK assert C.NK_enable_password_safe('wrong_password') == DeviceErrorCode.WRONG_PASSWORD assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK @@ -61,7 +66,7 @@ def test_password_safe_slot_status(C): def test_issue_device_locks_on_second_key_generation_in_sequence(C): - if is_pro_rtm_07(C): + if is_pro_rtm_07(C) or is_pro_rtm_08(C): pytest.skip("issue to register: device locks up " "after below commands sequence (reinsertion fixes), skipping for now") assert C.NK_build_aes_key(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK @@ -75,6 +80,19 @@ def test_regenerate_aes_key(C): assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK +def test_enable_password_safe_after_factory_reset(C): + assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK + assert C.NK_factory_reset(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK + wait(10) + if is_storage(C): + assert C.NK_clear_new_sd_card_warning(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK + enable_password_safe_result = C.NK_enable_password_safe(DefaultPasswords.USER) + assert enable_password_safe_result == DeviceErrorCode.STATUS_AES_DEC_FAILED \ + or is_storage(C) and enable_password_safe_result == DeviceErrorCode.WRONG_PASSWORD + assert C.NK_build_aes_key(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK + assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK + + @pytest.mark.xfail(reason="NK Pro firmware bug: regenerating AES key command not always results in cleared slot data") def test_destroy_password_safe(C): """ @@ -96,6 +114,7 @@ def test_destroy_password_safe(C): assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK assert C.NK_build_aes_key(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK + assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK assert gs(C.NK_get_password_safe_slot_name(0)) != 'slotname1' @@ -242,6 +261,7 @@ def test_HOTP_token(C): assert hotp_code != 0 assert C.NK_get_last_command_status() == DeviceErrorCode.STATUS_OK + def test_HOTP_counters(C): """ # https://tools.ietf.org/html/rfc4226#page-32 @@ -285,6 +305,7 @@ def test_HOTP_64bit_counter(C): assert C.NK_write_hotp_slot(slot_number, 'python_test', RFC_SECRET, t, use_8_digits, False, False, "", DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK code_device = str(C.NK_get_hotp_code(slot_number)) + code_device = '0'+code_device if len(code_device) < 6 else code_device dev_res += (t, code_device) lib_res += (t, lib_at(t)) assert dev_res == lib_res @@ -310,14 +331,18 @@ def test_TOTP_64bit_time(C): assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK assert C.NK_totp_set_time(t) == DeviceErrorCode.STATUS_OK code_device = str((C.NK_get_totp_code(slot_number, T, 0, 30))) + code_device = '0'+code_device if len(code_device) < 6 else code_device dev_res += (t, code_device) lib_res += (t, lib_at(t)) assert dev_res == lib_res -@pytest.mark.xfail(reason="NK Pro: possible firmware bug or communication issue: set time command not always changes the time on stick thus failing this test, " - "this does not influence normal use since setting time is not done every TOTP code request" - "Rarely fail occurs on NK Storage") +@pytest.mark.xfail(reason="NK Pro: Test fails in 50% of cases due to test vectors set 1 second before interval count change" + "Here time is changed on seconds side only and miliseconds part is not being reset apparently" + "This results in available time to test of half a second on average, thus 50% failed cases" + "With disabled two first test vectors test passess 10/10 times" + "Fail may also occurs on NK Storage with lower occurrency since it needs less time to execute " + "commands") @pytest.mark.parametrize("PIN_protection", [False, True, ]) def test_TOTP_RFC_usepin(C, PIN_protection): slot_number = 1 @@ -338,8 +363,8 @@ def test_TOTP_RFC_usepin(C, PIN_protection): # Mode: Sha1, time step X=30 test_data = [ #Time T (hex) TOTP - (59, 0x1, 94287082), - (1111111109, 0x00000000023523EC, 7081804), + (59, 0x1, 94287082), # Warning - test vector time 1 second before interval count changes + (1111111109, 0x00000000023523EC, 7081804), # Warning - test vector time 1 second before interval count changes (1111111111, 0x00000000023523ED, 14050471), (1234567890, 0x000000000273EF07, 89005924), (2000000000, 0x0000000003F940AA, 69279037), @@ -359,6 +384,7 @@ def test_TOTP_RFC_usepin(C, PIN_protection): correct += expected_code == code_from_device assert data == responses or correct == len(test_data) + def test_get_slot_names(C): C.NK_set_debug(True) assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK @@ -455,8 +481,6 @@ def test_read_write_config(C): def test_factory_reset(C): - if is_storage(C): - pytest.skip('Recovery not implemented for NK Storage') C.NK_set_debug(True) 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 @@ -473,6 +497,8 @@ def test_factory_reset(C): assert C.NK_build_aes_key(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK assert C.NK_enable_password_safe(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK + if is_storage(C): + assert C.NK_clear_new_sd_card_warning(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK def test_get_status(C): @@ -486,3 +512,235 @@ def test_get_serial_number(C): sn = gs(sn) assert len(sn) > 0 print(('Serial number of the device: ', sn)) + + +@pytest.mark.parametrize("secret", ['000001', '00'*10+'ff', '00'*19+'ff', '000102', + '00'*29+'ff', '00'*39+'ff', '002EF43F51AFA97BA2B46418768123C9E1809A5B' ]) +def test_OTP_secret_started_from_null(C, secret): + """ + NK Pro 0.8+, NK Storage 0.43+ + """ + skip_if_device_version_lower_than({'S': 43, 'P': 8}) + if len(secret) > 40: + # feature: 320 bit long secret handling + skip_if_device_version_lower_than({'S': 44, 'P': 8}) + + oath = pytest.importorskip("oath") + lib_at = lambda t: oath.hotp(secret, t, format='dec6') + PIN_protection = False + use_8_digits = False + slot_number = 1 + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_config(255, 255, 255, PIN_protection, not PIN_protection, + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + dev_res = [] + lib_res = [] + for t in range(1,5): + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_hotp_slot(slot_number, 'null_secret', secret, t, use_8_digits, False, False, "", + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + code_device = str(C.NK_get_hotp_code(slot_number)) + code_device = '0'+code_device if len(code_device) < 6 else code_device + dev_res += (t, code_device) + lib_res += (t, lib_at(t)) + assert dev_res == lib_res + + +@pytest.mark.parametrize("counter", [0, 3, 7, 0xffff, + 0xffffffff, + 0xffffffffffffffff] ) +def test_HOTP_slots_read_write_counter(C, counter): + """ + Write different counters to all HOTP slots, read code and compare with 3rd party + :param counter: + """ + if counter >= 1e7: + # Storage does not handle counters longer than 7 digits + skip_if_device_version_lower_than({'P': 7}) + + secret = RFC_SECRET + oath = pytest.importorskip("oath") + lib_at = lambda t: oath.hotp(secret, t, format='dec6') + PIN_protection = False + use_8_digits = False + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_config(255, 255, 255, PIN_protection, not PIN_protection, + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + dev_res = [] + lib_res = [] + for slot_number in range(3): + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_hotp_slot(slot_number, 'HOTP rw' + str(slot_number), secret, counter, use_8_digits, False, False, "", + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + code_device = str(C.NK_get_hotp_code(slot_number)) + code_device = '0'+code_device if len(code_device) < 6 else code_device + dev_res += (counter, code_device) + lib_res += (counter, lib_at(counter)) + assert dev_res == lib_res + + +@pytest.mark.parametrize("period", [30,60] ) +@pytest.mark.parametrize("time", range(21,70,20) ) +def test_TOTP_slots_read_write_at_time_period(C, time, period): + """ + Write to all TOTP slots with specified period, read code at specified time + and compare with 3rd party + """ + secret = RFC_SECRET + oath = pytest.importorskip("oath") + lib_at = lambda t: oath.totp(RFC_SECRET, t=t, period=period) + PIN_protection = False + use_8_digits = False + T = 0 + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_config(255, 255, 255, PIN_protection, not PIN_protection, + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + dev_res = [] + lib_res = [] + for slot_number in range(15): + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_totp_slot(slot_number, 'TOTP rw' + str(slot_number), secret, period, use_8_digits, False, False, "", + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_totp_set_time(time) == DeviceErrorCode.STATUS_OK + code_device = str(C.NK_get_totp_code(slot_number, T, 0, period)) + code_device = '0'+code_device if len(code_device) < 6 else code_device + dev_res += (time, code_device) + lib_res += (time, lib_at(time)) + assert dev_res == lib_res + + +@pytest.mark.parametrize("secret", [RFC_SECRET, 2*RFC_SECRET, '12'*10, '12'*30] ) +def test_TOTP_secrets(C, secret): + ''' + NK Pro 0.8+, NK Storage 0.44+ + ''' + skip_if_device_version_lower_than({'S': 44, 'P': 8}) + + if is_pro_rtm_07(C) and len(secret)>20*2: #*2 since secret is in hex + pytest.skip("Secret lengths over 20 bytes are not supported by NK Pro 0.7 ") + slot_number = 0 + time = 0 + period = 30 + oath = pytest.importorskip("oath") + lib_at = lambda t: oath.totp(secret, t=t, period=period) + PIN_protection = False + use_8_digits = False + T = 0 + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_config(255, 255, 255, PIN_protection, not PIN_protection, + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + dev_res = [] + lib_res = [] + assert C.NK_write_totp_slot(slot_number, 'secret' + str(len(secret)), secret, period, use_8_digits, False, False, "", + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_totp_set_time(time) == DeviceErrorCode.STATUS_OK + code_device = str(C.NK_get_totp_code(slot_number, T, 0, period)) + code_device = '0'+code_device if len(code_device) < 6 else code_device + dev_res += (time, code_device) + lib_res += (time, lib_at(time)) + assert dev_res == lib_res + + +@pytest.mark.parametrize("secret", [RFC_SECRET, 2*RFC_SECRET, '12'*10, '12'*30] ) +def test_HOTP_secrets(C, secret): + """ + NK Pro 0.8+, NK Storage 0.44+ + feature needed: support for 320bit secrets + """ + skip_if_device_version_lower_than({'S': 44, 'P': 8}) + + slot_number = 0 + counter = 0 + oath = pytest.importorskip("oath") + lib_at = lambda t: oath.hotp(secret, counter=t) + PIN_protection = False + use_8_digits = False + T = 0 + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_config(255, 255, 255, PIN_protection, not PIN_protection, + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + dev_res = [] + lib_res = [] + assert C.NK_write_hotp_slot(slot_number, 'secret' + str(len(secret)), secret, counter, use_8_digits, False, False, "", + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + code_device = str(C.NK_get_hotp_code(slot_number)) + code_device = '0'+code_device if len(code_device) < 6 else code_device + dev_res += (counter, code_device) + lib_res += (counter, lib_at(counter)) + assert dev_res == lib_res + + +def test_special_double_press(C): + """ + requires manual check after function run + double press each of num-, scroll-, caps-lock and check inserted OTP codes (each 1st should be 755224) + on nkpro 0.7 scrolllock should do nothing, on nkpro 0.8+ should return OTP code + """ + secret = RFC_SECRET + counter = 0 + PIN_protection = False + use_8_digits = False + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_config(0, 1, 2, PIN_protection, not PIN_protection, + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + for slot_number in range(3): + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_hotp_slot(slot_number, 'double' + str(slot_number), secret, counter, use_8_digits, False, False, "", + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + # requires manual check + + +def test_edit_OTP_slot(C): + """ + should change slots counter and name without changing its secret (using null secret for second update) + """ + # counter does not reset under Storage v0.43 + skip_if_device_version_lower_than({'S': 44, 'P': 7}) + + secret = RFC_SECRET + counter = 0 + PIN_protection = False + use_8_digits = False + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_config(255, 255, 255, PIN_protection, not PIN_protection, + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + slot_number = 0 + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + first_name = 'edit slot' + assert C.NK_write_hotp_slot(slot_number, first_name, secret, counter, use_8_digits, False, False, "", + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert gs(C.NK_get_hotp_slot_name(slot_number)) == first_name + + + first_code = C.NK_get_hotp_code(slot_number) + changed_name = 'changedname' + empty_secret = '' + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_hotp_slot(slot_number, changed_name, empty_secret, counter, use_8_digits, False, False, "", + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + second_code = C.NK_get_hotp_code(slot_number) + assert first_code == second_code + assert gs(C.NK_get_hotp_slot_name(slot_number)) == changed_name + + +@pytest.mark.skip +@pytest.mark.parametrize("secret", ['31323334353637383930'*2,'31323334353637383930'*4] ) +def test_TOTP_codes_from_nitrokeyapp(secret, C): + """ + Helper test for manual TOTP check of written secret by Nitrokey App + Destined to run by hand + """ + slot_number = 0 + PIN_protection = False + period = 30 + assert C.NK_first_authenticate(DefaultPasswords.ADMIN, DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + assert C.NK_write_config(255, 255, 255, PIN_protection, not PIN_protection, + DefaultPasswords.ADMIN_TEMP) == DeviceErrorCode.STATUS_OK + code_device = str(C.NK_get_totp_code(slot_number, 0, 0, period)) + code_device = '0'+code_device if len(code_device) < 6 else code_device + + oath = pytest.importorskip("oath") + lib_at = lambda : oath.totp(secret, period=period) + print (lib_at()) + assert lib_at() == code_device diff --git a/unittest/test_storage.py b/unittest/test_storage.py index 01276ce..a1c59aa 100644 --- a/unittest/test_storage.py +++ b/unittest/test_storage.py @@ -1,9 +1,10 @@ -import pytest +import pprint -from misc import ffi, gs, wait, cast_pointer_to_tuple -from constants import DefaultPasswords, DeviceErrorCode, RFC_SECRET, LibraryErrors +import pytest -import pprint +from conftest import skip_if_device_version_lower_than +from constants import DefaultPasswords, DeviceErrorCode +from misc import gs, wait pprint = pprint.PrettyPrinter(indent=4).pprint @@ -22,6 +23,7 @@ def get_dict_from_dissect(status): def test_get_status_storage(C): + skip_if_device_version_lower_than({'S': 43}) status_pointer = C.NK_get_status_storage_as_string() assert C.NK_get_last_command_status() == DeviceErrorCode.STATUS_OK status_string = gs(status_pointer) @@ -32,6 +34,7 @@ def test_get_status_storage(C): def test_sd_card_usage(C): + skip_if_device_version_lower_than({'S': 43}) data_pointer = C.NK_get_SD_usage_data_as_string() assert C.NK_get_last_command_status() == DeviceErrorCode.STATUS_OK data_string = gs(data_pointer) @@ -41,11 +44,13 @@ def test_sd_card_usage(C): def test_encrypted_volume_unlock(C): + skip_if_device_version_lower_than({'S': 43}) assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK assert C.NK_unlock_encrypted_volume(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK def test_encrypted_volume_unlock_hidden(C): + skip_if_device_version_lower_than({'S': 43}) hidden_volume_password = 'hiddenpassword' assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK assert C.NK_unlock_encrypted_volume(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK @@ -54,6 +59,7 @@ def test_encrypted_volume_unlock_hidden(C): @pytest.mark.skip(reason='hangs device, to report') def test_encrypted_volume_setup_multiple_hidden(C): + skip_if_device_version_lower_than({'S': 43}) hidden_volume_password = 'hiddenpassword' p = lambda i: hidden_volume_password + str(i) assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK @@ -67,25 +73,30 @@ def test_encrypted_volume_setup_multiple_hidden(C): def test_unencrypted_volume_set_read_only(C): + skip_if_device_version_lower_than({'S': 43}) assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK assert C.NK_set_unencrypted_read_only(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK def test_unencrypted_volume_set_read_write(C): + skip_if_device_version_lower_than({'S': 43}) assert C.NK_lock_device() == DeviceErrorCode.STATUS_OK assert C.NK_set_unencrypted_read_write(DefaultPasswords.USER) == DeviceErrorCode.STATUS_OK def test_export_firmware(C): + skip_if_device_version_lower_than({'S': 43}) assert C.NK_export_firmware(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK def test_clear_new_sd_card_notification(C): + skip_if_device_version_lower_than({'S': 43}) assert C.NK_clear_new_sd_card_warning(DefaultPasswords.ADMIN) == DeviceErrorCode.STATUS_OK @pytest.mark.skip def test_fill_SD_card(C): + skip_if_device_version_lower_than({'S': 43}) status = C.NK_fill_SD_card_with_random_data(DefaultPasswords.ADMIN) assert status == DeviceErrorCode.STATUS_OK or status == DeviceErrorCode.BUSY while 1: @@ -97,12 +108,14 @@ def test_fill_SD_card(C): def test_get_busy_progress_on_idle(C): + skip_if_device_version_lower_than({'S': 43}) value = C.NK_get_progress_bar_value() assert value == -1 assert C.NK_get_last_command_status() == DeviceErrorCode.STATUS_OK def test_change_update_password(C): + skip_if_device_version_lower_than({'S': 43}) wrong_password = 'aaaaaaaaaaa' assert C.NK_change_update_password(wrong_password, DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.WRONG_PASSWORD assert C.NK_change_update_password(DefaultPasswords.UPDATE, DefaultPasswords.UPDATE_TEMP) == DeviceErrorCode.STATUS_OK @@ -110,5 +123,6 @@ def test_change_update_password(C): def test_send_startup(C): + skip_if_device_version_lower_than({'S': 43}) time_seconds_from_epoch = 0 # FIXME set proper date assert C.NK_send_startup(time_seconds_from_epoch) == DeviceErrorCode.STATUS_OK |