c2aaf2edd1
Signed-off-by: Caolán McNamara <caolan.mcnamara@collabora.com> Change-Id: Ifb47d264a91a55d8bd565b1251a95973ddb8f5ec
518 lines
18 KiB
C++
518 lines
18 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
|
|
/*
|
|
* Copyright the Collabora Online contributors.
|
|
*
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include <iostream>
|
|
#include <iomanip>
|
|
#include <pwd.h>
|
|
#include <sstream>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sysexits.h>
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
|
|
#include <openssl/rand.h>
|
|
#include <openssl/evp.h>
|
|
|
|
#include <Poco/Crypto/RSAKey.h>
|
|
#include <Poco/Exception.h>
|
|
#include <Poco/File.h>
|
|
#include <Poco/Util/Application.h>
|
|
#include <Poco/Util/HelpFormatter.h>
|
|
#include <Poco/Util/Option.h>
|
|
#include <Poco/Util/OptionSet.h>
|
|
#include <Poco/Util/XMLConfiguration.h>
|
|
|
|
#include <common/ConfigUtil.hpp>
|
|
#include <Util.hpp>
|
|
#include <Crypto.hpp>
|
|
|
|
using Poco::Util::Application;
|
|
using Poco::Util::HelpFormatter;
|
|
using Poco::Util::Option;
|
|
using Poco::Util::OptionSet;
|
|
using Poco::Util::XMLConfiguration;
|
|
|
|
#define MIN_PWD_SALT_LENGTH 20
|
|
#define MIN_PWD_ITERATIONS 1000
|
|
#define MIN_PWD_HASH_LENGTH 20
|
|
|
|
bool EnableExperimental = false;
|
|
|
|
class CoolConfig final: public XMLConfiguration
|
|
{
|
|
public:
|
|
CoolConfig()
|
|
{}
|
|
};
|
|
|
|
struct AdminConfig
|
|
{
|
|
private:
|
|
unsigned _pwdSaltLength = 128;
|
|
unsigned _pwdIterations = 10000;
|
|
unsigned _pwdHashLength = 128;
|
|
public:
|
|
|
|
void setPwdSaltLength(unsigned pwdSaltLength) { _pwdSaltLength = pwdSaltLength; }
|
|
unsigned getPwdSaltLength() const { return _pwdSaltLength; }
|
|
void setPwdIterations(unsigned pwdIterations) { _pwdIterations = pwdIterations; }
|
|
unsigned getPwdIterations() const { return _pwdIterations; }
|
|
void setPwdHashLength(unsigned pwdHashLength) { _pwdHashLength = pwdHashLength; }
|
|
unsigned getPwdHashLength() const { return _pwdHashLength; }
|
|
};
|
|
|
|
// Config tool to change coolwsd configuration (coolwsd.xml)
|
|
class Config: public Application
|
|
{
|
|
// Display help information on the console
|
|
void displayHelp();
|
|
|
|
CoolConfig _coolConfig;
|
|
|
|
AdminConfig _adminConfig;
|
|
|
|
public:
|
|
static std::string ConfigFile;
|
|
static std::string OldConfigFile;
|
|
static std::string SupportKeyString;
|
|
static bool SupportKeyStringProvided;
|
|
static std::uint64_t AnonymizationSalt;
|
|
static bool AnonymizationSaltProvided;
|
|
static bool Write;
|
|
|
|
protected:
|
|
void defineOptions(OptionSet&) override;
|
|
void handleOption(const std::string&, const std::string&) override;
|
|
int main(const std::vector<std::string>&) override;
|
|
};
|
|
|
|
std::string Config::ConfigFile =
|
|
#if ENABLE_DEBUG
|
|
DEBUG_ABSSRCDIR
|
|
#else
|
|
COOLWSD_CONFIGDIR
|
|
#endif
|
|
"/coolwsd.xml";
|
|
|
|
std::string Config::OldConfigFile = "/etc/loolwsd/loolwsd.xml";
|
|
bool Config::Write = false;
|
|
|
|
std::string Config::SupportKeyString;
|
|
bool Config::SupportKeyStringProvided = false;
|
|
std::uint64_t Config::AnonymizationSalt = 0;
|
|
bool Config::AnonymizationSaltProvided = false;
|
|
|
|
int MigrateConfig(std::string, std::string, bool);
|
|
|
|
void Config::displayHelp()
|
|
{
|
|
HelpFormatter helpFormatter(options());
|
|
helpFormatter.setCommand(commandName());
|
|
helpFormatter.setUsage("COMMAND [OPTIONS]");
|
|
helpFormatter.setHeader("coolconfig - Configuration tool for Collabora Online.\n"
|
|
"\n"
|
|
"Some options make sense only with a specific command.\n\n"
|
|
"Options:");
|
|
|
|
helpFormatter.format(std::cout);
|
|
|
|
// Command list
|
|
std::cout << std::endl
|
|
<< "Commands: " << std::endl
|
|
<< " migrateconfig [--old-config-file=<path>] [--config-file=<path>] [--write]" << std::endl
|
|
<< " anonymize [string-1]...[string-n]" << std::endl
|
|
<< " set-admin-password" << std::endl;
|
|
if (config::isSupportKeyEnabled())
|
|
{
|
|
std::cout << " set-support-key" << std::endl;
|
|
}
|
|
std::cout << " set <key> <value>" << std::endl
|
|
<< " generate-proof-key" << std::endl
|
|
<< " update-system-template" << std::endl << std::endl;
|
|
}
|
|
|
|
void Config::defineOptions(OptionSet& optionSet)
|
|
{
|
|
Application::defineOptions(optionSet);
|
|
|
|
optionSet.addOption(Option("help", "h", "Show this usage information.")
|
|
.required(false)
|
|
.repeatable(false));
|
|
optionSet.addOption(Option("config-file", "", "Specify configuration file path manually.")
|
|
.required(false)
|
|
.repeatable(false)
|
|
.argument("path"));
|
|
optionSet.addOption(Option("old-config-file", "", "Specify configuration file path to migrate manually.")
|
|
.required(false)
|
|
.repeatable(false)
|
|
.argument("path"));
|
|
|
|
optionSet.addOption(Option("pwd-salt-length", "", "Length of the salt to use to hash password [set-admin-password].")
|
|
.required(false)
|
|
.repeatable(false)
|
|
.argument("number"));
|
|
optionSet.addOption(Option("pwd-iterations", "", "Number of iterations to do in PKDBF2 password hashing [set-admin-password].")
|
|
.required(false)
|
|
.repeatable(false)
|
|
.argument("number"));
|
|
optionSet.addOption(Option("pwd-hash-length", "", "Length of password hash to generate [set-admin-password].")
|
|
.required(false)
|
|
.repeatable(false)
|
|
.argument("number"));
|
|
|
|
if (config::isSupportKeyEnabled())
|
|
{
|
|
optionSet.addOption(Option("support-key", "", "Specify the support key [set-support-key].")
|
|
.required(false)
|
|
.repeatable(false)
|
|
.argument("key"));
|
|
}
|
|
|
|
optionSet.addOption(Option("anonymization-salt", "", "Anonymize strings with the given 64-bit salt instead of the one in the config file.")
|
|
.required(false)
|
|
.repeatable(false)
|
|
.argument("salt"));
|
|
|
|
optionSet.addOption(Option("write", "", "Write migrated configuration.")
|
|
.required(false)
|
|
.repeatable(false));
|
|
}
|
|
|
|
void Config::handleOption(const std::string& optionName, const std::string& optionValue)
|
|
{
|
|
Application::handleOption(optionName, optionValue);
|
|
if (optionName == "help")
|
|
{
|
|
displayHelp();
|
|
Util::forcedExit(EX_OK);
|
|
}
|
|
else if (optionName == "config-file")
|
|
{
|
|
ConfigFile = optionValue;
|
|
}
|
|
else if (optionName == "old-config-file")
|
|
{
|
|
OldConfigFile = optionValue;
|
|
}
|
|
else if (optionName == "pwd-salt-length")
|
|
{
|
|
unsigned len = std::stoi(optionValue);
|
|
if (len < MIN_PWD_SALT_LENGTH)
|
|
{
|
|
len = MIN_PWD_SALT_LENGTH;
|
|
std::cout << "Password salt length adjusted to minimum " << len << std::endl;
|
|
}
|
|
_adminConfig.setPwdSaltLength(len);
|
|
}
|
|
else if (optionName == "pwd-iterations")
|
|
{
|
|
unsigned len = std::stoi(optionValue);
|
|
if (len < MIN_PWD_ITERATIONS)
|
|
{
|
|
len = MIN_PWD_ITERATIONS;
|
|
std::cout << "Password iteration adjusted to minimum " << len << std::endl;
|
|
}
|
|
_adminConfig.setPwdIterations(len);
|
|
}
|
|
else if (optionName == "pwd-hash-length")
|
|
{
|
|
unsigned len = std::stoi(optionValue);
|
|
if (len < MIN_PWD_HASH_LENGTH)
|
|
{
|
|
len = MIN_PWD_HASH_LENGTH;
|
|
std::cout << "Password hash length adjusted to minimum " << len << std::endl;
|
|
}
|
|
_adminConfig.setPwdHashLength(len);
|
|
}
|
|
else if (optionName == "support-key")
|
|
{
|
|
SupportKeyString = optionValue;
|
|
SupportKeyStringProvided = true;
|
|
}
|
|
else if (optionName == "anonymization-salt")
|
|
{
|
|
AnonymizationSalt = std::stoull(optionValue);
|
|
AnonymizationSaltProvided = true;
|
|
std::cout << "Anonymization Salt: [" << AnonymizationSalt << "]." << std::endl;
|
|
}
|
|
else if (optionName == "write")
|
|
{
|
|
Write = true;
|
|
}
|
|
}
|
|
|
|
// coverity[root_function] : don't warn about uncaught exceptions
|
|
int Config::main(const std::vector<std::string>& args)
|
|
{
|
|
if (args.empty())
|
|
{
|
|
std::cerr << "Nothing to do." << std::endl;
|
|
displayHelp();
|
|
return EX_NOINPUT;
|
|
}
|
|
|
|
int retval = EX_OK;
|
|
bool changed = false;
|
|
_coolConfig.load(ConfigFile);
|
|
|
|
if (args[0] == "set-admin-password")
|
|
{
|
|
#if HAVE_PKCS5_PBKDF2_HMAC
|
|
std::vector<unsigned char> pwdhash(_adminConfig.getPwdHashLength());
|
|
std::vector<unsigned char> salt(_adminConfig.getPwdSaltLength());
|
|
RAND_bytes(salt.data(), _adminConfig.getPwdSaltLength());
|
|
std::stringstream stream;
|
|
|
|
// Ask for admin username
|
|
std::string adminUser;
|
|
std::cout << "Enter admin username [admin]: ";
|
|
std::getline(std::cin, adminUser);
|
|
if (adminUser.empty())
|
|
{
|
|
adminUser = "admin";
|
|
}
|
|
|
|
// Ask for user password
|
|
termios oldTermios;
|
|
tcgetattr(STDIN_FILENO, &oldTermios);
|
|
termios newTermios = oldTermios;
|
|
// Disable user input mirroring on console for password input
|
|
newTermios.c_lflag &= ~ECHO;
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &newTermios);
|
|
std::string adminPwd;
|
|
std::cout << "Enter admin password: ";
|
|
std::getline(std::cin, adminPwd);
|
|
std::string reAdminPwd;
|
|
std::cout << std::endl << "Confirm admin password: ";
|
|
std::getline(std::cin, reAdminPwd);
|
|
std::cout << std::endl;
|
|
// Set the termios to old state
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &oldTermios);
|
|
if (adminPwd != reAdminPwd)
|
|
{
|
|
std::cout << "Password mismatch." << std::endl;
|
|
return EX_DATAERR;
|
|
}
|
|
|
|
// Do the magic !
|
|
PKCS5_PBKDF2_HMAC(adminPwd.c_str(), -1,
|
|
salt.data(), _adminConfig.getPwdSaltLength(),
|
|
_adminConfig.getPwdIterations(),
|
|
EVP_sha512(),
|
|
_adminConfig.getPwdHashLength(), pwdhash.data());
|
|
|
|
// Make salt randomness readable
|
|
for (unsigned j = 0; j < _adminConfig.getPwdSaltLength(); ++j)
|
|
stream << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(salt[j]);
|
|
const std::string saltHash = stream.str();
|
|
|
|
// Clear our used hex stream to make space for password hash
|
|
stream.str("");
|
|
stream.clear();
|
|
|
|
// Make the hashed password readable
|
|
for (unsigned j = 0; j < _adminConfig.getPwdHashLength(); ++j)
|
|
stream << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(pwdhash[j]);
|
|
const std::string passwordHash = stream.str();
|
|
|
|
std::stringstream pwdConfigValue("pbkdf2.sha512.", std::ios_base::in | std::ios_base::out | std::ios_base::ate);
|
|
pwdConfigValue << std::to_string(_adminConfig.getPwdIterations()) << '.';
|
|
pwdConfigValue << saltHash << '.' << passwordHash;
|
|
_coolConfig.setString("admin_console.username", adminUser);
|
|
_coolConfig.setString("admin_console.secure_password[@desc]",
|
|
"Salt and password hash combination generated using PBKDF2 with SHA512 digest.");
|
|
_coolConfig.setString("admin_console.secure_password", pwdConfigValue.str());
|
|
|
|
changed = true;
|
|
#else
|
|
std::cerr << "This application was compiled with old OpenSSL. Operation not supported. You can use plain text password in /etc/coolwsd/coolwsd.xml." << std::endl;
|
|
return EX_UNAVAILABLE;
|
|
#endif
|
|
}
|
|
else if (config::isSupportKeyEnabled() && args[0] == "set-support-key")
|
|
{
|
|
std::string supportKeyString;
|
|
if (SupportKeyStringProvided)
|
|
supportKeyString = SupportKeyString;
|
|
else
|
|
{
|
|
std::cout << "Enter support key: ";
|
|
std::getline(std::cin, supportKeyString);
|
|
}
|
|
|
|
if (!supportKeyString.empty())
|
|
{
|
|
SupportKey key(supportKeyString);
|
|
if (!key.verify())
|
|
std::cerr << "Invalid key\n";
|
|
else {
|
|
int validDays = key.validDaysRemaining();
|
|
if (validDays <= 0)
|
|
std::cerr << "Valid but expired key\n";
|
|
else
|
|
{
|
|
std::cerr << "Valid for " << validDays << " days - setting to config\n";
|
|
_coolConfig.setString("support_key", supportKeyString);
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::cerr << "Removing empty support key\n";
|
|
_coolConfig.remove("support_key");
|
|
changed = true;
|
|
}
|
|
}
|
|
else if (args[0] == "set")
|
|
{
|
|
if (args.size() == 3)
|
|
{
|
|
// args[1] = key
|
|
// args[2] = value
|
|
if (_coolConfig.has(args[1]))
|
|
{
|
|
const std::string val = _coolConfig.getString(args[1]);
|
|
std::cout << "Previous value found in config file: \"" << val << '"' << std::endl;
|
|
std::cout << "Changing value to: \"" << args[2] << '"' << std::endl;
|
|
}
|
|
else
|
|
{
|
|
std::cout << "No property, \"" << args[1] << "\"," << " found in config file." << std::endl;
|
|
std::cout << "Adding it as new with value: \"" << args[2] << '"' << std::endl;
|
|
}
|
|
_coolConfig.setString(args[1], args[2]);
|
|
changed = true;
|
|
}
|
|
else
|
|
std::cerr << "set expects a key and value as arguments" << std::endl
|
|
<< "Eg: " << std::endl
|
|
<< " set logging.level trace" << std::endl;
|
|
|
|
}
|
|
else if (args[0] == "update-system-template")
|
|
{
|
|
const char command[] = "coolwsd-systemplate-setup /opt/cool/systemplate " LO_PATH " >/dev/null 2>&1";
|
|
std::cout << "Running the following command:" << std::endl
|
|
<< command << std::endl;
|
|
|
|
retval = system(command);
|
|
if (retval != 0)
|
|
std::cerr << "Error when executing command." << std::endl;
|
|
}
|
|
else if (args[0] == "anonymize")
|
|
{
|
|
if (!AnonymizationSaltProvided)
|
|
{
|
|
const std::string val = _coolConfig.getString("logging.anonymize.anonymization_salt");
|
|
AnonymizationSalt = std::stoull(val);
|
|
std::cout << "Anonymization Salt: [" << AnonymizationSalt << "]." << std::endl;
|
|
}
|
|
|
|
for (std::size_t i = 1; i < args.size(); ++i)
|
|
{
|
|
std::cout << '[' << args[i] << "]: " << Util::anonymizeUrl(args[i], AnonymizationSalt) << std::endl;
|
|
}
|
|
}
|
|
else if (args[0] == "migrateconfig")
|
|
{
|
|
std::cout << "Migrating old configuration from " << OldConfigFile << " to " << ConfigFile << "." << std::endl;
|
|
if (!Write)
|
|
std::cout << "This is a dry run, no changes are written to file." << std::endl;
|
|
std::cout << std::endl;
|
|
const std::string OldConfigMigrated = OldConfigFile + ".migrated";
|
|
Poco::File AlreadyMigrated(OldConfigMigrated);
|
|
if (AlreadyMigrated.exists())
|
|
{
|
|
std::cout << "Migration already performed, file " + OldConfigMigrated + " exists. Aborting." << std::endl;
|
|
}
|
|
else
|
|
{
|
|
const int Result = MigrateConfig(OldConfigFile, ConfigFile, Write);
|
|
if (Result == 0)
|
|
{
|
|
std::cout << "Successful migration." << std::endl;
|
|
if (Write)
|
|
{
|
|
Poco::File ConfigToRename(OldConfigFile);
|
|
ConfigToRename.renameTo(OldConfigMigrated);
|
|
}
|
|
}
|
|
else
|
|
std::cout << "Migration of old configuration failed." << std::endl;
|
|
}
|
|
}
|
|
else if (args[0] == "generate-proof-key")
|
|
{
|
|
std::string proofKeyPath =
|
|
#if ENABLE_DEBUG
|
|
DEBUG_ABSSRCDIR
|
|
#else
|
|
COOLWSD_CONFIGDIR
|
|
#endif
|
|
"/proof_key";
|
|
|
|
#if !ENABLE_DEBUG
|
|
struct passwd* pwd;
|
|
pwd = getpwnam(COOL_USER_ID);
|
|
if (pwd == NULL)
|
|
{
|
|
std::cerr << "User '" COOL_USER_ID
|
|
"' does not exist. Please reinstall coolwsd package, or in case of manual "
|
|
"installation from source, create the '" COOL_USER_ID "' user manually."
|
|
<< std::endl;
|
|
return EX_NOUSER;
|
|
}
|
|
#endif
|
|
|
|
Poco::File proofKeyFile(proofKeyPath);
|
|
if (!proofKeyFile.exists())
|
|
{
|
|
Poco::Crypto::RSAKey proofKey =
|
|
Poco::Crypto::RSAKey(Poco::Crypto::RSAKey::KeyLength::KL_2048,
|
|
Poco::Crypto::RSAKey::Exponent::EXP_LARGE);
|
|
proofKey.save(proofKeyPath + ".pub", proofKeyPath, "" /*no password*/);
|
|
#if !ENABLE_DEBUG
|
|
if (chmod(proofKeyPath.c_str(), S_IRUSR | S_IWUSR) != 0)
|
|
std::cerr << "Changing mode of " + proofKeyPath + " failed: " << strerror(errno) << std::endl;
|
|
if (chown(proofKeyPath.c_str(), pwd->pw_uid, -1) != 0)
|
|
std::cerr << "Changing owner of " + proofKeyPath + " failed: " << strerror(errno) << std::endl;
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
std::cerr << proofKeyPath << " exists already. New proof key was not generated."
|
|
<< std::endl;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::cerr << "No such command, \"" << args[0] << '"' << std::endl;
|
|
displayHelp();
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
std::cout << "Saving configuration to : " << ConfigFile << " ..." << std::endl;
|
|
_coolConfig.save(ConfigFile);
|
|
std::cout << "Saved" << std::endl;
|
|
}
|
|
|
|
return retval;
|
|
}
|
|
|
|
// coverity[root_function] : don't warn about uncaught exceptions
|
|
POCO_APP_MAIN(Config);
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|