diff --git a/.gitignore b/.gitignore index 21aa5e16..77b6050a 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ tests/*.log tests/*.trs tests/test-suite.log tests/test_BlockSpecial +tests/test_PasswordRAMStore tests/test_PipeCapture tests/test_dummy xmldocs.make diff --git a/tests/Makefile.am b/tests/Makefile.am index 1274cab5..0c1cfb61 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -11,8 +11,9 @@ LDADD = \ # Programs to be built by "make check" check_PROGRAMS = \ - test_dummy \ - test_BlockSpecial \ + test_dummy \ + test_BlockSpecial \ + test_PasswordRAMStore \ test_PipeCapture # Test cases to be run by "make check" @@ -25,6 +26,11 @@ test_BlockSpecial_LDADD = \ $(top_builddir)/src/BlockSpecial.$(OBJEXT) \ $(LDADD) +test_PasswordRAMStore_SOURCES = test_PasswordRAMStore.cc +test_PasswordRAMStore_LDADD = \ + $(top_builddir)/src/PasswordRAMStore.$(OBJEXT) \ + $(LDADD) + test_PipeCapture_SOURCES = test_PipeCapture.cc test_PipeCapture_LDADD = \ $(top_builddir)/src/PipeCapture.$(OBJEXT) \ diff --git a/tests/test_PasswordRAMStore.cc b/tests/test_PasswordRAMStore.cc new file mode 100644 index 00000000..78a43609 --- /dev/null +++ b/tests/test_PasswordRAMStore.cc @@ -0,0 +1,265 @@ +/* Copyright (C) 2017 Mike Fleetwood + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +/* Test PasswordRAMStore + * + * WARNING: + * This unit testing only calls the public API of the PasswordRAMStore so would normally + * be black box testing, however knowledge of the implementation is used to look through + * the API to the internals making this white box testing. This is so that the hidden + * behaviour of zeroing password storing memory before and after use can be tested. + * FIXME: Can't currently test memory is zeroed when the password store is destroyed + * because destructor zeros memory AND removes it from the process address space. + * + * WARNING: + * Each test fixture would normally initialise separate resources to make the tests + * independent of each other. However the password store is a single long lived shared + * resource. Therefore, so that each test fixture is independent of all the others, the + * password store must be returned to it's original state of being empty before each + * fixture completes. + * + * Reference: + * Google Test, Advanced Guide, Sharing Resources Between Tests in the Same Test Case + * https://github.com/google/googletest/blob/master/googletest/docs/AdvancedGuide.md#sharing-resources-between-tests-in-the-same-test-case + */ + +#include "PasswordRAMStore.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +namespace GParted +{ + +// Generate repeatable unique keys +static const char * gen_key( unsigned int i ) +{ + static char buf[14]; + snprintf( buf, sizeof( buf ), "key%u", i ); + return buf; +} + +// Generate repeatable "passwords" exactly 20 characters long +static const char * gen_passwd( unsigned int i ) +{ + static char buf[21]; + snprintf( buf, sizeof( buf ), "password%03u ", i ); + return buf; +} + +static bool mem_is_zero( const char * mem, size_t len ) +{ + while ( len-- > 0 ) + { + if ( *mem++ != '\0' ) + { + return false; + } + } + return true; +} + +// Explicit test fixture class for common setup and sharing of the underlying password +// store address. +class PasswordRAMStoreTest : public ::testing::Test +{ +protected: + PasswordRAMStoreTest() : looked_up_pw( NULL ) {}; + + static void SetUpTestCase(); + + static const char * protected_mem; + + std::string pw; + const char * looked_up_pw; + size_t looked_up_len; +}; + +// Initialise test case class static member. +const char * PasswordRAMStoreTest::protected_mem = NULL; + +const size_t ProtectedMemSize = 4096; // [Implementation knowledge: size] + +// Common test case initialisation discovering the underlying password store address. +void PasswordRAMStoreTest::SetUpTestCase() +{ + const Glib::ustring key = "key-setup"; + bool success = PasswordRAMStore::insert( key, "" ); + ASSERT_TRUE( success ) << __func__ << "(): Insert \"" << key << "\" password failed"; + + // First password is stored at the start of the locked memory. + // [Implementation knowledge: locked memory layout] + protected_mem = PasswordRAMStore::lookup( key ); + ASSERT_TRUE( protected_mem != NULL ) << __func__ + << "(): Find \"" << key << "\" password failed"; + + success = PasswordRAMStore::erase( key ); + ASSERT_TRUE( success ) << __func__ << "(): Erase \"" << key << "\" password failed"; +} + +TEST_F( PasswordRAMStoreTest, Initialisation ) +{ + // Test locked memory is initialised with all zeros. + EXPECT_TRUE( mem_is_zero( protected_mem, ProtectedMemSize ) ); +} + +TEST_F( PasswordRAMStoreTest, UnknownPasswordLookup ) +{ + // Test lookup of non-existent password fails. + looked_up_pw = PasswordRAMStore::lookup( "key-unknown" ); + EXPECT_TRUE( looked_up_pw == NULL ); +} + +TEST_F( PasswordRAMStoreTest, UnknownPasswordErasure ) +{ + // Test erase non-existent password fails. + EXPECT_FALSE( PasswordRAMStore::erase( "key-unknown" ) ); +} + +TEST_F( PasswordRAMStoreTest, SinglePassword ) +{ + // Test a single password can be stored, looked up and erased (and zeroed). + pw = "password"; + EXPECT_TRUE( PasswordRAMStore::insert( "key-single", pw.c_str() ) ); + + looked_up_pw = PasswordRAMStore::lookup( "key-single" ); + EXPECT_STREQ( pw.c_str(), looked_up_pw ); + + looked_up_len = strlen( looked_up_pw ); + EXPECT_TRUE( PasswordRAMStore::erase( "key-single" ) ); + EXPECT_TRUE( mem_is_zero( looked_up_pw, looked_up_len ) ); + + EXPECT_TRUE( mem_is_zero( protected_mem, ProtectedMemSize ) ); +} + +TEST_F( PasswordRAMStoreTest, DuplicatePassword ) +{ + // Test storing a password with a duplicate key fails (and single password can be + // erased and zeroed). + pw = "password"; + EXPECT_TRUE( PasswordRAMStore::insert( "key-single", pw.c_str() ) ); + + looked_up_pw = PasswordRAMStore::lookup( "key-single" ); + EXPECT_STREQ( pw.c_str(), looked_up_pw ); + + EXPECT_FALSE( PasswordRAMStore::insert( "key-single", pw.c_str() ) ); + + looked_up_len = strlen( looked_up_pw ); + EXPECT_TRUE( PasswordRAMStore::erase( "key-single" ) ); + EXPECT_TRUE( mem_is_zero( looked_up_pw, looked_up_len ) ); + + EXPECT_TRUE( mem_is_zero( protected_mem, ProtectedMemSize ) ); +} + +TEST_F( PasswordRAMStoreTest, OneHundredPasswordsForwards ) +{ + // Test 100, 20 character passwords can be stored, looked up and erased (and + // zeroed). Passwords are erased forwards (first stored to last stored). + unsigned int i; + for ( i = 0 ; i < 100 ; i ++ ) + { + pw = gen_passwd( i ); + EXPECT_TRUE( PasswordRAMStore::insert( gen_key(i), pw.c_str() ) ); + } + + for ( i = 0 ; i < 100 ; i ++ ) + { + pw = gen_passwd( i ); + looked_up_pw = PasswordRAMStore::lookup( gen_key(i) ); + EXPECT_STREQ( pw.c_str(), looked_up_pw ); + } + + for ( i = 0 ; i < 100 ; i ++ ) + { + pw = gen_passwd( i ); + looked_up_pw = PasswordRAMStore::lookup( gen_key(i) ); + looked_up_len = strlen( looked_up_pw ); + EXPECT_TRUE( PasswordRAMStore::erase( gen_key(i) ) ); + EXPECT_TRUE( mem_is_zero( looked_up_pw, looked_up_len ) ); + } + + EXPECT_TRUE( mem_is_zero( protected_mem, ProtectedMemSize ) ); +} + +TEST_F( PasswordRAMStoreTest, OneHundredPasswordsBackwards ) +{ + // Test 100, 20 character passwords can be stored, looked up and erased (and + // zeroed). Passwords are erased backwards (last stored to first stored). + unsigned int i; + for ( i = 0 ; i < 100 ; i ++ ) + { + pw = gen_passwd( i ); + EXPECT_TRUE( PasswordRAMStore::insert( gen_key(i), pw.c_str() ) ); + } + + for ( i = 0 ; i < 100 ; i ++ ) + { + pw = gen_passwd( i ); + looked_up_pw = PasswordRAMStore::lookup( gen_key(i) ); + EXPECT_STREQ( pw.c_str(), looked_up_pw ); + } + + for ( i = 100; i-- > 0 ; ) + { + pw = gen_passwd( i ); + looked_up_pw = PasswordRAMStore::lookup( gen_key(i) ); + looked_up_len = strlen( looked_up_pw ); + EXPECT_TRUE( PasswordRAMStore::erase( gen_key(i) ) ); + EXPECT_TRUE( mem_is_zero( looked_up_pw, looked_up_len ) ); + } + + EXPECT_TRUE( mem_is_zero( protected_mem, ProtectedMemSize ) ); +} + +TEST_F( PasswordRAMStoreTest, LongPassword ) +{ + // Test a 4095 byte password can be stored and looked up (and erased and zeroed). + // [Implementation knowledge: size] + pw = std::string( ProtectedMemSize-1, 'X' ); + + EXPECT_TRUE( PasswordRAMStore::insert( "key-long", pw.c_str() ) ); + + looked_up_pw = PasswordRAMStore::lookup( "key-long" ); + EXPECT_STREQ( pw.c_str(), looked_up_pw ); + + looked_up_len = strlen( looked_up_pw ); + EXPECT_TRUE( PasswordRAMStore::erase( "key-long" ) ); + EXPECT_TRUE( mem_is_zero( looked_up_pw, looked_up_len ) ); + + EXPECT_TRUE( mem_is_zero( protected_mem, ProtectedMemSize ) ); +} + +TEST_F( PasswordRAMStoreTest, TooLongPassword ) +{ + // Test a 4096 byte password can't be stored nor looked up or erased. + // [Implementation knowledge: size] + std::string pw = std::string( ProtectedMemSize, 'X' ); + + EXPECT_FALSE( PasswordRAMStore::insert( "key-too-long", pw.c_str() ) ); + EXPECT_TRUE( mem_is_zero( protected_mem, ProtectedMemSize ) ); + + looked_up_pw = PasswordRAMStore::lookup( "key-too-long" ); + EXPECT_TRUE( looked_up_pw == NULL ); + + EXPECT_FALSE( PasswordRAMStore::erase( "key-long" ) ); + EXPECT_TRUE( mem_is_zero( protected_mem, ProtectedMemSize ) ); +} + +} // namespace GParted