If CRYFS_FRONTEND=noninteractive is set in the environment, assume we're used by a tool and:

- Don't ask for config. Use default settings for everything that is not specified as command line parameter.
- Don't ask for password confirmation. Password only has to be passed in once to stdin.
This commit is contained in:
Sebastian Messmer 2016-02-21 01:34:21 +01:00
parent 346baf8e9b
commit 9c83d3b2a4
15 changed files with 135 additions and 48 deletions

View File

@ -1,3 +1,9 @@
Version 0.9.3 (unreleased)
---------------
* It's easier for tools and scripts to use CryFS:
If an environment variable CRYFS_FRONTEND=noninteractive is set, we don't ask for options (but take default values for everything that's not specified on command line).
Furthermore, we won't ask for password confirmation when creating a file system but the password only has to be sent once to stdin.
Version 0.9.2
---------------
* Experimental support for installing CryFS on Mac OS X using homebrew

View File

@ -70,9 +70,16 @@ using cpputils::dynamic_pointer_move;
//TODO Performance difference when setting compiler parameter -maes for scrypt?
namespace cryfs {
const string Cli::CRYFS_FRONTEND_KEY = "CRYFS_FRONTEND";
const string Cli::CRYFS_FRONTEND_NONINTERACTIVE = "noninteractive";
Cli::Cli(RandomGenerator &keyGenerator, const SCryptSettings &scryptSettings, shared_ptr<Console> console, shared_ptr<HttpClient> httpClient):
_keyGenerator(keyGenerator), _scryptSettings(scryptSettings), _console(console), _httpClient(httpClient) {}
_keyGenerator(keyGenerator), _scryptSettings(scryptSettings), _console(console), _httpClient(httpClient), _noninteractive(false) {
char *frontend = std::getenv(CRYFS_FRONTEND_KEY.c_str());
if (frontend != nullptr && frontend == CRYFS_FRONTEND_NONINTERACTIVE) {
_noninteractive = true;
}
}
void Cli::_showVersion() {
cout << "CryFS Version " << version::VERSION_STRING << endl;
@ -110,19 +117,6 @@ namespace cryfs {
return true;
}
string Cli::_getPassword(function<string()> askPassword) {
string password = askPassword();
//Remove trailing newline
if (password[password.size()-1] == '\n') {
password.resize(password.size()-1);
}
//Check that password is valid
if (!_checkPassword(password)) {
throw std::runtime_error("Password invalid.");
}
return password;
}
string Cli::_askPasswordForExistingFilesystem() {
string password = _askPasswordFromStdin("Password: ");
while (!_checkPassword(password)) {
@ -158,6 +152,15 @@ namespace cryfs {
return true;
}
string Cli::_askPasswordNoninteractive() {
//TODO Test
string password = _askPasswordFromStdin("Password: ");
if (!_checkPassword(password)) {
throw std::runtime_error("Invalid password. Password cannot be empty.");
}
return password;
}
string Cli::_askPasswordFromStdin(const string &prompt) {
DontEchoStdinToStdoutRAII _stdin_input_is_hidden_as_long_as_this_is_in_scope;
@ -166,6 +169,11 @@ namespace cryfs {
std::getline(cin, result);
std::cout << std::endl;
//Remove trailing newline
if (result[result.size()-1] == '\n') {
result.resize(result.size()-1);
}
return result;
}
@ -180,10 +188,7 @@ namespace cryfs {
CryConfigFile Cli::_loadOrCreateConfig(const ProgramOptions &options) {
try {
auto configFile = _determineConfigFile(options);
auto config = CryConfigLoader(_console, _keyGenerator, _scryptSettings,
std::bind(&Cli::_getPassword, this, &Cli::_askPasswordForExistingFilesystem),
std::bind(&Cli::_getPassword, this, &Cli::_askPasswordForNewFilesystem),
options.cipher()).loadOrCreate(configFile);
auto config = _loadOrCreateConfigFile(configFile, options.cipher());
if (config == none) {
std::cerr << "Could not load config file. Did you enter the correct password?" << std::endl;
exit(1);
@ -195,6 +200,20 @@ namespace cryfs {
}
}
optional<CryConfigFile> Cli::_loadOrCreateConfigFile(const bf::path &configFilePath, const optional<string> &cipher) {
if (_noninteractive) {
return CryConfigLoader(_console, _keyGenerator, _scryptSettings,
&Cli::_askPasswordNoninteractive,
&Cli::_askPasswordNoninteractive,
cipher, _noninteractive).loadOrCreate(configFilePath);
} else {
return CryConfigLoader(_console, _keyGenerator, _scryptSettings,
&Cli::_askPasswordForExistingFilesystem,
&Cli::_askPasswordForNewFilesystem,
cipher, _noninteractive).loadOrCreate(configFilePath);
}
}
void Cli::_runFilesystem(const ProgramOptions &options) {
try {
auto blockStore = make_unique_ref<OnDiskBlockStore>(options.baseDir());
@ -269,6 +288,10 @@ namespace cryfs {
void Cli::_checkDirAccessible(const bf::path &dir, const std::string &name) {
if (!bf::exists(dir)) {
if (_noninteractive) {
//If we use the noninteractive frontend, don't ask whether to create the directory, but just fail.
throw std::runtime_error(name + " not found");
}
bool create = _console->askYesNo("Could not find " + name + ". Do you want to create it?");
if (create) {
if (!bf::create_directory(dir)) {

View File

@ -21,10 +21,11 @@ namespace cryfs {
private:
void _runFilesystem(const program_options::ProgramOptions &options);
CryConfigFile _loadOrCreateConfig(const program_options::ProgramOptions &options);
boost::optional<CryConfigFile> _loadOrCreateConfigFile(const boost::filesystem::path &configFilePath, const boost::optional<std::string> &cipher);
boost::filesystem::path _determineConfigFile(const program_options::ProgramOptions &options);
std::string _getPassword(std::function<std::string()> askPassword);
static std::string _askPasswordForExistingFilesystem();
static std::string _askPasswordForNewFilesystem();
static std::string _askPasswordNoninteractive();
static std::string _askPasswordFromStdin(const std::string &prompt);
static bool _confirmPassword(const std::string &password);
static bool _checkPassword(const std::string &password);
@ -39,10 +40,14 @@ namespace cryfs {
boost::optional<cpputils::unique_ref<CallAfterTimeout>> _createIdleCallback(boost::optional<double> minutes, std::function<void()> callback);
void _sanityCheckFilesystem(CryDevice *device);
static const std::string CRYFS_FRONTEND_KEY;
static const std::string CRYFS_FRONTEND_NONINTERACTIVE;
cpputils::RandomGenerator &_keyGenerator;
cpputils::SCryptSettings _scryptSettings;
std::shared_ptr<cpputils::Console> _console;
std::shared_ptr<cpputils::HttpClient> _httpClient;
bool _noninteractive;
DISALLOW_COPY_AND_ASSIGN(Cli);
};

View File

@ -12,8 +12,8 @@ using std::shared_ptr;
namespace cryfs {
constexpr const char *CryConfigConsole::DEFAULT_CIPHER;
CryConfigConsole::CryConfigConsole(shared_ptr<Console> console)
: _console(std::move(console)), _useDefaultSettings(none) {
CryConfigConsole::CryConfigConsole(shared_ptr<Console> console, bool noninteractive)
: _console(std::move(console)), _useDefaultSettings(noninteractive ? optional<bool>(true) : none) {
}
string CryConfigConsole::askCipher() {

View File

@ -9,7 +9,7 @@
namespace cryfs {
class CryConfigConsole final {
public:
CryConfigConsole(std::shared_ptr<cpputils::Console> console);
CryConfigConsole(std::shared_ptr<cpputils::Console> console, bool noninteractive);
CryConfigConsole(CryConfigConsole &&rhs) = default;
std::string askCipher();
@ -17,6 +17,7 @@ namespace cryfs {
static constexpr const char *DEFAULT_CIPHER = "aes-256-gcm";
private:
bool _checkUseDefaultSettings();
std::string _askCipher() const;

View File

@ -13,8 +13,8 @@ using boost::none;
namespace cryfs {
CryConfigCreator::CryConfigCreator(shared_ptr<Console> console, RandomGenerator &encryptionKeyGenerator)
:_console(console), _configConsole(console), _encryptionKeyGenerator(encryptionKeyGenerator) {
CryConfigCreator::CryConfigCreator(shared_ptr<Console> console, RandomGenerator &encryptionKeyGenerator, bool noninteractive)
:_console(console), _configConsole(console, noninteractive), _encryptionKeyGenerator(encryptionKeyGenerator) {
}
CryConfig CryConfigCreator::create(const optional<string> &cipherFromCommandLine) {

View File

@ -11,7 +11,7 @@
namespace cryfs {
class CryConfigCreator final {
public:
CryConfigCreator(std::shared_ptr<cpputils::Console> console, cpputils::RandomGenerator &encryptionKeyGenerator);
CryConfigCreator(std::shared_ptr<cpputils::Console> console, cpputils::RandomGenerator &encryptionKeyGenerator, bool noninteractive);
CryConfigCreator(CryConfigCreator &&rhs) = default;
CryConfig create(const boost::optional<std::string> &cipherFromCommandLine);

View File

@ -25,8 +25,8 @@ using namespace cpputils::logging;
namespace cryfs {
CryConfigLoader::CryConfigLoader(shared_ptr<Console> console, RandomGenerator &keyGenerator, const SCryptSettings &scryptSettings, function<string()> askPasswordForExistingFilesystem, function<string()> askPasswordForNewFilesystem, const optional<string> &cipherFromCommandLine)
: _creator(std::move(console), keyGenerator), _scryptSettings(scryptSettings),
CryConfigLoader::CryConfigLoader(shared_ptr<Console> console, RandomGenerator &keyGenerator, const SCryptSettings &scryptSettings, function<string()> askPasswordForExistingFilesystem, function<string()> askPasswordForNewFilesystem, const optional<string> &cipherFromCommandLine, bool noninteractive)
: _creator(std::move(console), keyGenerator, noninteractive), _scryptSettings(scryptSettings),
_askPasswordForExistingFilesystem(askPasswordForExistingFilesystem), _askPasswordForNewFilesystem(askPasswordForNewFilesystem),
_cipherFromCommandLine(cipherFromCommandLine) {
}

View File

@ -13,7 +13,7 @@ namespace cryfs {
class CryConfigLoader final {
public:
CryConfigLoader(std::shared_ptr<cpputils::Console> console, cpputils::RandomGenerator &keyGenerator, const cpputils::SCryptSettings &scryptSettings, std::function<std::string()> askPasswordForExistingFilesystem, std::function<std::string()> askPasswordForNewFilesystem, const boost::optional<std::string> &cipherFromCommandLine);
CryConfigLoader(std::shared_ptr<cpputils::Console> console, cpputils::RandomGenerator &keyGenerator, const cpputils::SCryptSettings &scryptSettings, std::function<std::string()> askPasswordForExistingFilesystem, std::function<std::string()> askPasswordForNewFilesystem, const boost::optional<std::string> &cipherFromCommandLine, bool noninteractive);
CryConfigLoader(CryConfigLoader &&rhs) = default;
boost::optional<CryConfigFile> loadOrCreate(const boost::filesystem::path &filename);

View File

@ -4,6 +4,7 @@ namespace bf = boost::filesystem;
using ::testing::Values;
using ::testing::WithParamInterface;
using ::testing::Return;
using ::testing::_;
using std::vector;
using cpputils::TempFile;
@ -116,10 +117,21 @@ TEST_P(CliTest_WrongEnvironment, MountDirIsBaseDir_BothRelative) {
TEST_P(CliTest_WrongEnvironment, BaseDir_DoesntExist) {
_basedir.remove();
ON_CALL(*console, askYesNo("Could not find base directory. Do you want to create it?")).WillByDefault(Return(false)); // ON_CALL and not EXPECT_CALL, because this is a death test (i.e. it is forked) and gmock EXPECT_CALL in fork children don't report to parents.
// ON_CALL and not EXPECT_CALL, because this is a death test (i.e. it is forked) and gmock EXPECT_CALL in fork children don't report to parents.
ON_CALL(*console, askYesNo("Could not find base directory. Do you want to create it?")).WillByDefault(Return(false));
Test_Run_Error("Error: base directory not found");
}
TEST_P(CliTest_WrongEnvironment, BaseDir_DoesntExist_Noninteractive) {
_basedir.remove();
// We can't set an EXPECT_CALL().Times(0), because this is a death test (i.e. it is forked) and gmock EXPECT_CALL in fork children don't report to parents.
// So we set a default answer that shouldn't crash and check it's not called by checking that it crashes.
ON_CALL(*console, askYesNo("Could not find base directory. Do you want to create it?")).WillByDefault(Return(true));
::setenv("CRYFS_FRONTEND", "noninteractive", 1);
Test_Run_Error("Error: base directory not found");
::unsetenv("CRYFS_FRONTEND");
}
TEST_P(CliTest_WrongEnvironment, BaseDir_DoesntExist_Create) {
if (!GetParam().runningInForeground) {return;} // TODO Make this work also if run in background (see CliTest::EXPECT_RUN_SUCCESS)
_basedir.remove();
@ -163,10 +175,21 @@ TEST_P(CliTest_WrongEnvironment, BaseDir_NoPermission) {
TEST_P(CliTest_WrongEnvironment, MountDir_DoesntExist) {
_mountdir.remove();
ON_CALL(*console, askYesNo("Could not find mount directory. Do you want to create it?")).WillByDefault(Return(false)); // ON_CALL and not EXPECT_CALL, because this is a death test (i.e. it is forked) and gmock EXPECT_CALL in fork children don't report to parents.
// ON_CALL and not EXPECT_CALL, because this is a death test (i.e. it is forked) and gmock EXPECT_CALL in fork children don't report to parents.
ON_CALL(*console, askYesNo("Could not find mount directory. Do you want to create it?")).WillByDefault(Return(false));
Test_Run_Error("Error: mount directory not found");
}
TEST_P(CliTest_WrongEnvironment, MountDir_DoesntExist_Noninteractive) {
_mountdir.remove();
// We can't set an EXPECT_CALL().Times(0), because this is a death test (i.e. it is forked) and gmock EXPECT_CALL in fork children don't report to parents.
// So we set a default answer that shouldn't crash and check it's not called by checking that it crashes.
ON_CALL(*console, askYesNo("Could not find base directory. Do you want to create it?")).WillByDefault(Return(true));
::setenv("CRYFS_FRONTEND", "noninteractive", 1);
Test_Run_Error("Error: mount directory not found");
::unsetenv("CRYFS_FRONTEND");
}
TEST_P(CliTest_WrongEnvironment, MountDir_DoesntExist_Create) {
if (!GetParam().runningInForeground) {return;} // TODO Make this work also if run in background (see CliTest::EXPECT_RUN_SUCCESS)
_mountdir.remove();

View File

@ -28,10 +28,12 @@ class CryConfigConsoleTest: public ::testing::Test {
public:
CryConfigConsoleTest()
: console(make_shared<MockConsole>()),
cryconsole(console) {
cryconsole(console, false),
noninteractiveCryconsole(console, true) {
}
shared_ptr<MockConsole> console;
CryConfigConsole cryconsole;
CryConfigConsole noninteractiveCryconsole;
};
class CryConfigConsoleTest_Cipher: public CryConfigConsoleTest {};
@ -52,6 +54,13 @@ TEST_F(CryConfigConsoleTest_Cipher, ChooseDefaultCipher) {
EXPECT_EQ(CryConfigConsole::DEFAULT_CIPHER, cipher);
}
TEST_F(CryConfigConsoleTest_Cipher, ChooseDefaultCipherWhenNoninteractiveEnvironment) {
EXPECT_CALL(*console, askYesNo(HasSubstr("default"))).Times(0);
EXPECT_CALL(*console, ask(HasSubstr("block cipher"), _)).Times(0);
string cipher = noninteractiveCryconsole.askCipher();
EXPECT_EQ(CryConfigConsole::DEFAULT_CIPHER, cipher);
}
class CryConfigConsoleTest_Cipher_Choose: public CryConfigConsoleTest_Cipher, public ::testing::WithParamInterface<string> {
public:
string cipherName = GetParam();

View File

@ -28,10 +28,12 @@ class CryConfigCreatorTest: public ::testing::Test {
public:
CryConfigCreatorTest()
: console(make_shared<MockConsole>()),
creator(console, cpputils::Random::PseudoRandom()) {
creator(console, cpputils::Random::PseudoRandom(), false),
noninteractiveCreator(console, cpputils::Random::PseudoRandom(), true) {
}
shared_ptr<MockConsole> console;
CryConfigCreator creator;
CryConfigCreator noninteractiveCreator;
};
#define EXPECT_ASK_FOR_CIPHER() \
@ -51,6 +53,11 @@ TEST_F(CryConfigCreatorTest, DoesNotAskForCipherIfSpecified) {
CryConfig config = creator.create(string("aes-256-gcm"));
}
TEST_F(CryConfigCreatorTest, DoesNotAskForCipherIfNoninteractive) {
EXPECT_DOES_NOT_ASK_FOR_CIPHER();
CryConfig config = noninteractiveCreator.create(none);
}
TEST_F(CryConfigCreatorTest, ChoosesEmptyRootBlobId) {
EXPECT_ASK_FOR_CIPHER().WillOnce(ChooseAnyCipher());
CryConfig config = creator.create(none);

View File

@ -26,41 +26,39 @@ namespace boost {
}
#include <boost/optional/optional_io.hpp>
//TODO Test loading with same/different --cipher argument
class CryConfigLoaderTest: public ::testing::Test, public TestWithMockConsole {
public:
CryConfigLoaderTest(): file(false) {}
CryConfigLoader loader(const string &password, const optional<string> &cipher = none) {
auto askPassword = [password] {return password;};
return CryConfigLoader(mockConsole(), cpputils::Random::PseudoRandom(), SCrypt::TestSettings, askPassword, askPassword, cipher);
CryConfigLoader loader(const string &password, bool noninteractive, const optional<string> &cipher = none) {
auto askPassword = [password] { return password;};
return CryConfigLoader(mockConsole(), cpputils::Random::PseudoRandom(), SCrypt::TestSettings, askPassword, askPassword, cipher, noninteractive);
}
CryConfigFile Create(const string &password = "mypassword", const optional<string> &cipher = none) {
CryConfigFile Create(const string &password = "mypassword", const optional<string> &cipher = none, bool noninteractive = false) {
EXPECT_FALSE(file.exists());
return loader(password, cipher).loadOrCreate(file.path()).value();
return loader(password, noninteractive, cipher).loadOrCreate(file.path()).value();
}
optional<CryConfigFile> Load(const string &password = "mypassword", const optional<string> &cipher = none) {
optional<CryConfigFile> Load(const string &password = "mypassword", const optional<string> &cipher = none, bool noninteractive = false) {
EXPECT_TRUE(file.exists());
return loader(password, cipher).loadOrCreate(file.path());
return loader(password, noninteractive, cipher).loadOrCreate(file.path());
}
void CreateWithRootBlob(const string &rootBlob, const string &password = "mypassword") {
auto cfg = loader(password).loadOrCreate(file.path()).value();
auto cfg = loader(password, false).loadOrCreate(file.path()).value();
cfg.config()->SetRootBlob(rootBlob);
cfg.save();
}
void CreateWithCipher(const string &cipher, const string &password = "mypassword") {
auto cfg = loader(password).loadOrCreate(file.path()).value();
auto cfg = loader(password, false).loadOrCreate(file.path()).value();
cfg.config()->SetCipher(cipher);
cfg.save();
}
void CreateWithEncryptionKey(const string &encKey, const string &password = "mypassword") {
auto cfg = loader(password).loadOrCreate(file.path()).value();
auto cfg = loader(password, false).loadOrCreate(file.path()).value();
cfg.config()->SetEncryptionKey(encKey);
cfg.save();
}
@ -86,9 +84,19 @@ TEST_F(CryConfigLoaderTest, DoesntLoadIfWrongPassword) {
}
TEST_F(CryConfigLoaderTest, DoesntLoadIfDifferentCipher) {
Create("mypassword", string("aes-256-gcm"));
Create("mypassword", string("aes-256-gcm"), false);
try {
Load("mypassword", string("aes-256-cfb"));
Load("mypassword", string("aes-256-cfb"), false);
EXPECT_TRUE(false); // Should throw exception
} catch (const std::runtime_error &e) {
EXPECT_EQ(string("Filesystem uses aes-256-gcm cipher and not aes-256-cfb as specified."), e.what());
}
}
TEST_F(CryConfigLoaderTest, DoesntLoadIfDifferentCipher_Noninteractive) {
Create("mypassword", string("aes-256-gcm"), true);
try {
Load("mypassword", string("aes-256-cfb"), true);
EXPECT_TRUE(false); // Should throw exception
} catch (const std::runtime_error &e) {
EXPECT_EQ(string("Filesystem uses aes-256-gcm cipher and not aes-256-cfb as specified."), e.what());
@ -100,6 +108,11 @@ TEST_F(CryConfigLoaderTest, DoesLoadIfSameCipher) {
Load("mypassword", string("aes-256-gcm"));
}
TEST_F(CryConfigLoaderTest, DoesLoadIfSameCipher_Noninteractive) {
Create("mypassword", string("aes-128-gcm"), true);
Load("mypassword", string("aes-128-gcm"), true);
}
TEST_F(CryConfigLoaderTest, RootBlob_Load) {
CreateWithRootBlob("rootblobid");
auto loaded = Load().value();

View File

@ -37,7 +37,7 @@ public:
CryConfigFile loadOrCreateConfig() {
auto askPassword = [] {return "mypassword";};
return CryConfigLoader(mockConsole(), Random::PseudoRandom(), SCrypt::TestSettings, askPassword, askPassword, none).loadOrCreate(config.path()).value();
return CryConfigLoader(mockConsole(), Random::PseudoRandom(), SCrypt::TestSettings, askPassword, askPassword, none, true).loadOrCreate(config.path()).value();
}
unique_ref<OnDiskBlockStore> blockStore() {

View File

@ -28,7 +28,7 @@ public:
unique_ref<Device> createDevice() override {
auto blockStore = cpputils::make_unique_ref<FakeBlockStore>();
auto askPassword = [] {return "mypassword";};
auto config = CryConfigLoader(mockConsole(), Random::PseudoRandom(), SCrypt::TestSettings, askPassword, askPassword, none)
auto config = CryConfigLoader(mockConsole(), Random::PseudoRandom(), SCrypt::TestSettings, askPassword, askPassword, none, true)
.loadOrCreate(configFile.path()).value();
return make_unique_ref<CryDevice>(std::move(config), std::move(blockStore));
}