Replace RC4 credentials encryption with AES.
Starting with OpenSSL 3, RC4 is not enabled/accessible by default.
This commit is contained in:
parent
e416d28f7f
commit
12d630a50f
1 changed files with 183 additions and 40 deletions
|
@ -74,8 +74,8 @@ static std::vector<login_info> credentials;
|
|||
// Separate password entries with formfeed
|
||||
static const unsigned char CREDENTIAL_SEPARATOR = '\f';
|
||||
|
||||
static secure_buffer encrypt(const secure_buffer& text, const secure_buffer& key);
|
||||
static secure_buffer decrypt(const secure_buffer& text, const secure_buffer& key);
|
||||
static secure_buffer aes_encrypt(const secure_buffer& text, const secure_buffer& key);
|
||||
static secure_buffer aes_decrypt(const secure_buffer& text, const secure_buffer& key);
|
||||
static secure_buffer build_key(const std::string& server, const std::string& login);
|
||||
static secure_buffer escape(const secure_buffer& text);
|
||||
static secure_buffer unescape(const secure_buffer& text);
|
||||
|
@ -162,7 +162,7 @@ namespace preferences
|
|||
|
||||
if(!remember_password()) {
|
||||
if(!credentials.empty() && credentials[0].username == login_clean && credentials[0].server == server) {
|
||||
auto temp = decrypt(credentials[0].key, build_key(server, login_clean));
|
||||
auto temp = aes_decrypt(credentials[0].key, build_key(server, login_clean));
|
||||
return std::string(temp.begin(), temp.end());
|
||||
} else {
|
||||
return "";
|
||||
|
@ -174,7 +174,7 @@ namespace preferences
|
|||
if(cred == credentials.end()) {
|
||||
return "";
|
||||
}
|
||||
auto temp = decrypt(cred->key, build_key(server, login_clean));
|
||||
auto temp = aes_decrypt(cred->key, build_key(server, login_clean));
|
||||
return std::string(temp.begin(), temp.end());
|
||||
}
|
||||
|
||||
|
@ -187,7 +187,7 @@ namespace preferences
|
|||
secure_buffer temp(key.begin(), key.end());
|
||||
if(!remember_password()) {
|
||||
clear_credentials();
|
||||
credentials.emplace_back(login_clean, server, encrypt(temp, build_key(server, login_clean)));
|
||||
credentials.emplace_back(login_clean, server, aes_encrypt(temp, build_key(server, login_clean)));
|
||||
return;
|
||||
}
|
||||
auto cred = std::find_if(credentials.begin(), credentials.end(), [&](const login_info& cred) {
|
||||
|
@ -197,7 +197,7 @@ namespace preferences
|
|||
// This is equivalent to emplace_back, but also returns the iterator to the new element
|
||||
cred = credentials.emplace(credentials.end(), login_clean, server);
|
||||
}
|
||||
cred->key = encrypt(temp, build_key(server, login_clean));
|
||||
cred->key = aes_encrypt(temp, build_key(server, login_clean));
|
||||
}
|
||||
|
||||
void load_credentials()
|
||||
|
@ -213,7 +213,7 @@ namespace preferences
|
|||
filesystem::scoped_istream stream = filesystem::istream_file(cred_file, false);
|
||||
// Credentials file is a binary blob, so use streambuf iterator
|
||||
secure_buffer data((std::istreambuf_iterator<char>(*stream)), (std::istreambuf_iterator<char>()));
|
||||
data = decrypt(data, build_key("global", get_system_username()));
|
||||
data = aes_decrypt(data, build_key("global", get_system_username()));
|
||||
if(data.empty() || data[0] != CREDENTIAL_SEPARATOR) {
|
||||
ERR_CFG << "Invalid data in credentials file";
|
||||
return;
|
||||
|
@ -246,7 +246,7 @@ namespace preferences
|
|||
}
|
||||
try {
|
||||
filesystem::scoped_ostream credentials_file = filesystem::ostream_file(filesystem::get_credentials_file());
|
||||
secure_buffer encrypted = encrypt(credentials_data, build_key("global", get_system_username()));
|
||||
secure_buffer encrypted = aes_encrypt(credentials_data, build_key("global", get_system_username()));
|
||||
credentials_file->write(reinterpret_cast<const char*>(encrypted.data()), encrypted.size());
|
||||
} catch(const filesystem::io_exception&) {
|
||||
ERR_CFG << "error writing to credentials file '" << filesystem::get_credentials_file() << "'";
|
||||
|
@ -254,7 +254,14 @@ namespace preferences
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Key-stretching (bcrypt was recommended)
|
||||
/**
|
||||
* Fills a secure_buffer with 32 bytes of deterministically generated bytes, then overwrites it with the system login name, server login name, and server name.
|
||||
* If this is more than 32 bytes, then it's truncated. If it's less than 32 bytes, then the pre-generated bytes are used to pad it.
|
||||
*
|
||||
* @param server The server being logged into.
|
||||
* @param login The username being used to login.
|
||||
* @return secure_buffer The data to be used as the encryption key.
|
||||
*/
|
||||
secure_buffer build_key(const std::string& server, const std::string& login)
|
||||
{
|
||||
std::string sysname = get_system_username();
|
||||
|
@ -267,65 +274,201 @@ secure_buffer build_key(const std::string& server, const std::string& login)
|
|||
return result;
|
||||
}
|
||||
|
||||
static secure_buffer rc4_crypt(const secure_buffer& text, const secure_buffer& key)
|
||||
/**
|
||||
* Encrypts the value of @a plaintext using @a key and a hard coded IV using AES.
|
||||
* Max size of @a plaintext must not be larger than 1008 bytes.
|
||||
*
|
||||
* NOTE: This is not meant to provide strong protections against a determined attacker.
|
||||
* This is meant to hide the passwords from malware scanning files for passwords, family/friends poking around, etc.
|
||||
*
|
||||
* @param plaintext The original unencrypted data.
|
||||
* @param key The value to use to encrypt the data. See build_key() for key generation.
|
||||
* @return secure_buffer The encrypted data.
|
||||
*/
|
||||
static secure_buffer aes_encrypt(const secure_buffer& plaintext, const secure_buffer& key)
|
||||
{
|
||||
secure_buffer result(text.size(), '\0');
|
||||
#ifndef __APPLE__
|
||||
int outlen;
|
||||
int tmplen;
|
||||
int update_length;
|
||||
int extra_length;
|
||||
int total_length;
|
||||
// AES IV is generally 128 bits
|
||||
const unsigned char iv[] = {1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8};
|
||||
unsigned char encrypted_buffer[1024];
|
||||
|
||||
if(plaintext.size() > 1008)
|
||||
{
|
||||
ERR_CFG << "Cannot encrypt data larger than 1008 bytes.";
|
||||
return secure_buffer();
|
||||
}
|
||||
DBG_CFG << "Encrypting data with length: " << plaintext.size();
|
||||
|
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
|
||||
if(!ctx)
|
||||
{
|
||||
ERR_CFG << "AES EVP_CIPHER_CTX_new failed with error:";
|
||||
ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
|
||||
return secure_buffer();
|
||||
}
|
||||
|
||||
// TODO: use EVP_EncryptInit_ex2 once openssl 3.0 is more widespread
|
||||
EVP_EncryptInit_ex(ctx, EVP_rc4(), NULL, key.data(), NULL);
|
||||
if(EVP_EncryptUpdate(ctx, result.data(), &outlen, text.data(), text.size()) == 0)
|
||||
if(EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key.data(), iv) != 1)
|
||||
{
|
||||
ERR_CFG << "EVP_EncryptUpdate failed with error:";
|
||||
ERR_CFG << "AES EVP_EncryptInit_ex failed with error:";
|
||||
ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return secure_buffer();
|
||||
}
|
||||
if(EVP_EncryptFinal_ex(ctx, result.data() + outlen, &tmplen) == 0)
|
||||
|
||||
if(EVP_EncryptUpdate(ctx, encrypted_buffer, &update_length, plaintext.data(), plaintext.size()) != 1)
|
||||
{
|
||||
ERR_CFG << "EVP_EncryptFinal failed with error:";
|
||||
ERR_CFG << "AES EVP_EncryptUpdate failed with error:";
|
||||
ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return secure_buffer();
|
||||
}
|
||||
DBG_CFG << "Update length: " << update_length;
|
||||
|
||||
if(EVP_EncryptFinal_ex(ctx, encrypted_buffer + update_length, &extra_length) != 1)
|
||||
{
|
||||
ERR_CFG << "AES EVP_EncryptFinal failed with error:";
|
||||
ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return secure_buffer();
|
||||
}
|
||||
DBG_CFG << "Extra length: " << extra_length;
|
||||
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
|
||||
total_length = update_length+extra_length;
|
||||
secure_buffer result;
|
||||
for(int i = 0; i < total_length; i++)
|
||||
{
|
||||
result.push_back(encrypted_buffer[i]);
|
||||
}
|
||||
|
||||
DBG_CFG << "Successfully encrypted plaintext value of '" << utils::join(plaintext, "") << "' having length " << plaintext.size();
|
||||
DBG_CFG << "For a total encrypted length of: " << total_length;
|
||||
|
||||
return result;
|
||||
#else
|
||||
// the encrypted result gets padded to the next multiple of 16, not just the length of the input text
|
||||
size_t length = ((plaintext.size() / 16)+1)*16;
|
||||
secure_buffer result(length, '\0');
|
||||
size_t outWritten = 0;
|
||||
CCCryptorStatus ccStatus = CCCrypt(kCCDecrypt,
|
||||
kCCAlgorithmRC4,
|
||||
kCCOptionPKCS7Padding,
|
||||
key.data(),
|
||||
key.size(),
|
||||
nullptr,
|
||||
text.data(),
|
||||
text.size(),
|
||||
result.data(),
|
||||
result.size(),
|
||||
&outWritten);
|
||||
|
||||
CCCryptorStatus ccStatus = CCCrypt(kCCEncrypt,
|
||||
kCCAlgorithmAES128,
|
||||
kCCOptionPKCS7Padding,
|
||||
key.data(),
|
||||
key.size(),
|
||||
nullptr,
|
||||
plaintext.data(),
|
||||
plaintext.size(),
|
||||
result.data(),
|
||||
result.size(),
|
||||
&outWritten);
|
||||
|
||||
assert(ccStatus == kCCSuccess);
|
||||
assert(outWritten == text.size());
|
||||
#endif
|
||||
assert(outWritten == length);
|
||||
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
secure_buffer encrypt(const secure_buffer& text, const secure_buffer& key)
|
||||
/**
|
||||
* Same as aes_encrypt(), except of course it takes encrypted data as an argument and returns decrypted data.
|
||||
*/
|
||||
static secure_buffer aes_decrypt(const secure_buffer& encrypted, const secure_buffer& key)
|
||||
{
|
||||
return rc4_crypt(text, key);
|
||||
}
|
||||
#ifndef __APPLE__
|
||||
int update_length;
|
||||
int extra_length;
|
||||
int total_length;
|
||||
// AES IV is generally 128 bits
|
||||
const unsigned char iv[] = {1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8};
|
||||
unsigned char plaintext_buffer[1024];
|
||||
|
||||
secure_buffer decrypt(const secure_buffer& text, const secure_buffer& key)
|
||||
{
|
||||
auto buf = rc4_crypt(text, key);
|
||||
while(!buf.empty() && buf.back() == 0) {
|
||||
buf.pop_back();
|
||||
if(encrypted.size() > 1024)
|
||||
{
|
||||
ERR_CFG << "Cannot decrypt data larger than 1024 bytes.";
|
||||
return secure_buffer();
|
||||
}
|
||||
return buf;
|
||||
DBG_CFG << "Decrypting data with length: " << encrypted.size();
|
||||
|
||||
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
|
||||
if(!ctx)
|
||||
{
|
||||
ERR_CFG << "AES EVP_CIPHER_CTX_new failed with error:";
|
||||
ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
|
||||
return secure_buffer();
|
||||
}
|
||||
|
||||
// TODO: use EVP_DecryptInit_ex2 once openssl 3.0 is more widespread
|
||||
if(EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key.data(), iv) != 1)
|
||||
{
|
||||
ERR_CFG << "AES EVP_DecryptInit_ex failed with error:";
|
||||
ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return secure_buffer();
|
||||
}
|
||||
|
||||
if(EVP_DecryptUpdate(ctx, plaintext_buffer, &update_length, encrypted.data(), encrypted.size()) != 1)
|
||||
{
|
||||
ERR_CFG << "AES EVP_DecryptUpdate failed with error:";
|
||||
ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return secure_buffer();
|
||||
}
|
||||
DBG_CFG << "Update length: " << update_length;
|
||||
|
||||
if(EVP_DecryptFinal_ex(ctx, plaintext_buffer + update_length, &extra_length) != 1)
|
||||
{
|
||||
ERR_CFG << "AES EVP_DecryptFinal failed with error:";
|
||||
ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
return secure_buffer();
|
||||
}
|
||||
DBG_CFG << "Extra length: " << extra_length;
|
||||
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
|
||||
total_length = update_length+extra_length;
|
||||
secure_buffer result;
|
||||
for(int i = 0; i < total_length; i++)
|
||||
{
|
||||
result.push_back(plaintext_buffer[i]);
|
||||
}
|
||||
|
||||
DBG_CFG << "Successfully decrypted data to the value: " << utils::join(result, "");
|
||||
DBG_CFG << "For a total decrypted length of: " << total_length;
|
||||
|
||||
return result;
|
||||
#else
|
||||
size_t outWritten = 0;
|
||||
secure_buffer result(encrypted.size(), '\0');
|
||||
|
||||
CCCryptorStatus ccStatus = CCCrypt(kCCDecrypt,
|
||||
kCCAlgorithmAES128,
|
||||
kCCOptionPKCS7Padding,
|
||||
key.data(),
|
||||
key.size(),
|
||||
nullptr,
|
||||
encrypted.data(),
|
||||
encrypted.size(),
|
||||
result.data(),
|
||||
result.size(),
|
||||
&outWritten);
|
||||
|
||||
assert(ccStatus == kCCSuccess);
|
||||
assert(outWritten == result.size());
|
||||
|
||||
// the decrypted result is likely shorter than the encrypted data, so the extra padding needs to be removed.
|
||||
while(!result.empty() && result.back() == 0) {
|
||||
result.pop_back();
|
||||
}
|
||||
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
secure_buffer unescape(const secure_buffer& text)
|
||||
|
|
Loading…
Add table
Reference in a new issue