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 /unittest | |
parent | f60f2cf0144a91769a5fc00fac1314d2e00cdf0d (diff) | |
parent | e26c6da38c674d8ec37e402132dab823bd22bd36 (diff) | |
download | libnitrokey-ed5044da43172d86a1aa475473561a4818b7c69c.tar.gz libnitrokey-ed5044da43172d86a1aa475473561a4818b7c69c.tar.bz2 |
Merge pull request #53 from Nitrokey/nk_pro_0.8_authorization_fix-longer_secretv2.0
Support for Nitrokey Pro 0.8
Diffstat (limited to 'unittest')
-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 |
8 files changed, 548 insertions, 31 deletions
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 |