The Battle for Wesnoth  1.19.0-dev
credentials.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2017 - 2024
3  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
15 #include "credentials.hpp"
16 
17 #include "preferences/general.hpp"
19 #include "filesystem.hpp"
20 #include "log.hpp"
22 
23 #include <boost/algorithm/string.hpp>
24 
25 #include <algorithm>
26 #include <memory>
27 
28 #ifndef __APPLE__
29 #include <openssl/evp.h>
30 #include <openssl/err.h>
31 #else
32 #include <CommonCrypto/CommonCryptor.h>
33 #endif
34 
35 #ifdef _WIN32
36 #include <boost/range/iterator_range.hpp>
37 #include <windows.h>
38 #endif
39 
40 static lg::log_domain log_config("config");
41 #define DBG_CFG LOG_STREAM(debug , log_config)
42 #define ERR_CFG LOG_STREAM(err , log_config)
43 
44 class secure_buffer : public std::vector<unsigned char>
45 {
46 public:
47  template<typename... T> secure_buffer(T&&... args)
48  : vector<unsigned char>(std::forward<T>(args)...)
49  {}
51  {
52  std::fill(begin(), end(), '\0');
53  }
54 };
55 
56 struct login_info
57 {
58  std::string username, server;
60  login_info(const std::string& username, const std::string& server, const secure_buffer& key)
62  {}
63  login_info(const std::string& username, const std::string& server)
65  {}
66  std::size_t size() const
67  {
68  return 3 + username.size() + server.size() + key.size();
69  }
70 };
71 
72 static std::vector<login_info> credentials;
73 
74 // Separate password entries with formfeed
75 static const unsigned char CREDENTIAL_SEPARATOR = '\f';
76 
77 static secure_buffer aes_encrypt(const secure_buffer& text, const secure_buffer& key);
78 static secure_buffer aes_decrypt(const secure_buffer& text, const secure_buffer& key);
79 static secure_buffer build_key(const std::string& server, const std::string& login);
80 static secure_buffer escape(const secure_buffer& text);
81 static secure_buffer unescape(const secure_buffer& text);
82 
83 static std::string get_system_username()
84 {
85  std::string res;
86 #ifdef _WIN32
87  wchar_t buffer[300];
88  DWORD size = 300;
89  if(GetUserNameW(buffer, &size)) {
90  //size includes a terminating null character.
91  assert(size > 0);
92  res = unicode_cast<std::string>(boost::iterator_range<wchar_t*>(buffer, buffer + size - 1));
93  }
94 #else
95  if(char* const login = getenv("USER")) {
96  res = login;
97  }
98 #endif
99  return res;
100 }
101 
102 static void clear_credentials()
103 {
104  // Zero them before clearing.
105  // Probably overly paranoid, but doesn't hurt?
106  for(auto& cred : credentials) {
107  std::fill(cred.username.begin(), cred.username.end(), '\0');
108  std::fill(cred.server.begin(), cred.server.end(), '\0');
109  }
110  credentials.clear();
111 }
112 
113 static const std::string EMPTY_LOGIN = "@@";
114 
115 namespace preferences
116 {
117  std::string login()
118  {
119  std::string name = preferences::get("login", EMPTY_LOGIN);
120  if(name == EMPTY_LOGIN) {
121  name = get_system_username();
122  } else if(name.size() > 2 && name.front() == '@' && name.back() == '@') {
123  name = name.substr(1, name.size() - 2);
124  } else {
125  ERR_CFG << "malformed user credentials (did you manually edit the preferences file?)";
126  }
127  if(name.empty()) {
128  return "player";
129  }
130  return name;
131  }
132 
133  void set_login(const std::string& login)
134  {
135  auto login_clean = login;
136  boost::trim(login_clean);
137 
138  preferences::set("login", '@' + login_clean + '@');
139  }
140 
142  {
143  return preferences::get("remember_password", false);
144  }
145 
146  void set_remember_password(bool remember)
147  {
148  preferences::set("remember_password", remember);
149 
150  if(remember) {
152  } else {
154  }
155  }
156 
157  std::string password(const std::string& server, const std::string& login)
158  {
159  DBG_CFG << "Retrieving password for server: '" << server << "', login: '" << login << "'";
160  auto login_clean = login;
161  boost::trim(login_clean);
162 
163  if(!remember_password()) {
164  if(!credentials.empty() && credentials[0].username == login_clean && credentials[0].server == server) {
165  auto temp = aes_decrypt(credentials[0].key, build_key(server, login_clean));
166  return std::string(temp.begin(), temp.end());
167  } else {
168  return "";
169  }
170  }
171  auto cred = std::find_if(credentials.begin(), credentials.end(), [&](const login_info& cred) {
172  return cred.server == server && cred.username == login_clean;
173  });
174  if(cred == credentials.end()) {
175  return "";
176  }
177  auto temp = aes_decrypt(cred->key, build_key(server, login_clean));
178  return std::string(temp.begin(), temp.end());
179  }
180 
181  void set_password(const std::string& server, const std::string& login, const std::string& key)
182  {
183  DBG_CFG << "Setting password for server: '" << server << "', login: '" << login << "'";
184  auto login_clean = login;
185  boost::trim(login_clean);
186 
187  secure_buffer temp(key.begin(), key.end());
188  if(!remember_password()) {
190  credentials.emplace_back(login_clean, server, aes_encrypt(temp, build_key(server, login_clean)));
191  return;
192  }
193  auto cred = std::find_if(credentials.begin(), credentials.end(), [&](const login_info& cred) {
194  return cred.server == server && cred.username == login_clean;
195  });
196  if(cred == credentials.end()) {
197  // This is equivalent to emplace_back, but also returns the iterator to the new element
198  cred = credentials.emplace(credentials.end(), login_clean, server);
199  }
200  cred->key = aes_encrypt(temp, build_key(server, login_clean));
201  }
202 
204  {
205  if(!remember_password()) {
206  return;
207  }
209  std::string cred_file = filesystem::get_credentials_file();
210  if(!filesystem::file_exists(cred_file)) {
211  return;
212  }
213  filesystem::scoped_istream stream = filesystem::istream_file(cred_file, false);
214  // Credentials file is a binary blob, so use streambuf iterator
215  secure_buffer data((std::istreambuf_iterator<char>(*stream)), (std::istreambuf_iterator<char>()));
217  if(data.empty() || data[0] != CREDENTIAL_SEPARATOR) {
218  ERR_CFG << "Invalid data in credentials file";
219  return;
220  }
221  for(const std::string& elem : utils::split(std::string(data.begin(), data.end()), CREDENTIAL_SEPARATOR, utils::REMOVE_EMPTY)) {
222  std::size_t at = elem.find_last_of('@');
223  std::size_t eq = elem.find_first_of('=', at + 1);
224  if(at != std::string::npos && eq != std::string::npos) {
225  secure_buffer key(elem.begin() + eq + 1, elem.end());
226  credentials.emplace_back(elem.substr(0, at), elem.substr(at + 1, eq - at - 1), unescape(key));
227  }
228  }
229  }
230 
232  {
233  if(!remember_password()) {
235  return;
236  }
237  secure_buffer credentials_data;
238  for(const auto& cred : credentials) {
239  credentials_data.push_back(CREDENTIAL_SEPARATOR);
240  credentials_data.insert(credentials_data.end(), cred.username.begin(), cred.username.end());
241  credentials_data.push_back('@');
242  credentials_data.insert(credentials_data.end(), cred.server.begin(), cred.server.end());
243  credentials_data.push_back('=');
244  secure_buffer key_escaped = escape(cred.key);
245  credentials_data.insert(credentials_data.end(), key_escaped.begin(), key_escaped.end());
246  }
247  try {
249  secure_buffer encrypted = aes_encrypt(credentials_data, build_key("global", get_system_username()));
250  credentials_file->write(reinterpret_cast<const char*>(encrypted.data()), encrypted.size());
251  } catch(const filesystem::io_exception&) {
252  ERR_CFG << "error writing to credentials file '" << filesystem::get_credentials_file() << "'";
253  }
254  }
255 }
256 
257 /**
258  * 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.
259  * 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.
260  *
261  * @param server The server being logged into.
262  * @param login The username being used to login.
263  * @return secure_buffer The data to be used as the encryption key.
264  */
265 secure_buffer build_key(const std::string& server, const std::string& login)
266 {
267  std::string sysname = get_system_username();
268  secure_buffer result(std::max<std::size_t>(server.size() + login.size() + sysname.size(), 32));
269  unsigned char i = 0;
270  std::generate(result.begin(), result.end(), [&i]() {return 'x' ^ i++;});
271  std::copy(login.begin(), login.end(), result.begin());
272  std::copy(sysname.begin(), sysname.end(), result.begin() + login.size());
273  std::copy(server.begin(), server.end(), result.begin() + login.size() + sysname.size());
274  return result;
275 }
276 
277 /**
278  * Encrypts the value of @a plaintext using @a key and a hard coded IV using AES.
279  * Max size of @a plaintext must not be larger than 1008 bytes.
280  *
281  * NOTE: This is not meant to provide strong protections against a determined attacker.
282  * This is meant to hide the passwords from malware scanning files for passwords, family/friends poking around, etc.
283  *
284  * @param plaintext The original unencrypted data.
285  * @param key The value to use to encrypt the data. See build_key() for key generation.
286  * @return secure_buffer The encrypted data.
287  */
288 static secure_buffer aes_encrypt(const secure_buffer& plaintext, const secure_buffer& key)
289 {
290 #ifndef __APPLE__
291  int update_length;
292  int extra_length;
293  int total_length;
294  // AES IV is generally 128 bits
295  const unsigned char iv[] = {1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8};
296  unsigned char encrypted_buffer[1024];
297 
298  if(plaintext.size() > 1008)
299  {
300  ERR_CFG << "Cannot encrypt data larger than 1008 bytes.";
301  return secure_buffer();
302  }
303  DBG_CFG << "Encrypting data with length: " << plaintext.size();
304 
305  EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
306  if(!ctx)
307  {
308  ERR_CFG << "AES EVP_CIPHER_CTX_new failed with error:";
309  ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
310  return secure_buffer();
311  }
312 
313  // TODO: use EVP_EncryptInit_ex2 once openssl 3.0 is more widespread
314  if(EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key.data(), iv) != 1)
315  {
316  ERR_CFG << "AES EVP_EncryptInit_ex failed with error:";
317  ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
318  EVP_CIPHER_CTX_free(ctx);
319  return secure_buffer();
320  }
321 
322  if(EVP_EncryptUpdate(ctx, encrypted_buffer, &update_length, plaintext.data(), plaintext.size()) != 1)
323  {
324  ERR_CFG << "AES EVP_EncryptUpdate failed with error:";
325  ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
326  EVP_CIPHER_CTX_free(ctx);
327  return secure_buffer();
328  }
329  DBG_CFG << "Update length: " << update_length;
330 
331  if(EVP_EncryptFinal_ex(ctx, encrypted_buffer + update_length, &extra_length) != 1)
332  {
333  ERR_CFG << "AES EVP_EncryptFinal failed with error:";
334  ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
335  EVP_CIPHER_CTX_free(ctx);
336  return secure_buffer();
337  }
338  DBG_CFG << "Extra length: " << extra_length;
339 
340  EVP_CIPHER_CTX_free(ctx);
341 
342  total_length = update_length+extra_length;
343  secure_buffer result;
344  for(int i = 0; i < total_length; i++)
345  {
346  result.push_back(encrypted_buffer[i]);
347  }
348 
349  DBG_CFG << "Successfully encrypted plaintext value of '" << utils::join(plaintext, "") << "' having length " << plaintext.size();
350  DBG_CFG << "For a total encrypted length of: " << total_length;
351 
352  return result;
353 #else
354  size_t outWritten = 0;
355  secure_buffer result(plaintext.size(), '\0');
356 
357  CCCryptorStatus ccStatus = CCCrypt(kCCDecrypt,
358  kCCAlgorithmRC4,
359  kCCOptionPKCS7Padding,
360  key.data(),
361  key.size(),
362  nullptr,
363  plaintext.data(),
364  plaintext.size(),
365  result.data(),
366  result.size(),
367  &outWritten);
368 
369  assert(ccStatus == kCCSuccess);
370  assert(outWritten == plaintext.size());
371 
372  return result;
373 #endif
374 }
375 
376 /**
377  * Same as aes_encrypt(), except of course it takes encrypted data as an argument and returns decrypted data.
378  */
379 static secure_buffer aes_decrypt(const secure_buffer& encrypted, const secure_buffer& key)
380 {
381 #ifndef __APPLE__
382  int update_length;
383  int extra_length;
384  int total_length;
385  // AES IV is generally 128 bits
386  const unsigned char iv[] = {1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8};
387  unsigned char plaintext_buffer[1024];
388 
389  if(encrypted.size() > 1024)
390  {
391  ERR_CFG << "Cannot decrypt data larger than 1024 bytes.";
392  return secure_buffer();
393  }
394  DBG_CFG << "Decrypting data with length: " << encrypted.size();
395 
396  EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
397  if(!ctx)
398  {
399  ERR_CFG << "AES EVP_CIPHER_CTX_new failed with error:";
400  ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
401  return secure_buffer();
402  }
403 
404  // TODO: use EVP_DecryptInit_ex2 once openssl 3.0 is more widespread
405  if(EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key.data(), iv) != 1)
406  {
407  ERR_CFG << "AES EVP_DecryptInit_ex failed with error:";
408  ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
409  EVP_CIPHER_CTX_free(ctx);
410  return secure_buffer();
411  }
412 
413  if(EVP_DecryptUpdate(ctx, plaintext_buffer, &update_length, encrypted.data(), encrypted.size()) != 1)
414  {
415  ERR_CFG << "AES EVP_DecryptUpdate failed with error:";
416  ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
417  EVP_CIPHER_CTX_free(ctx);
418  return secure_buffer();
419  }
420  DBG_CFG << "Update length: " << update_length;
421 
422  if(EVP_DecryptFinal_ex(ctx, plaintext_buffer + update_length, &extra_length) != 1)
423  {
424  ERR_CFG << "AES EVP_DecryptFinal failed with error:";
425  ERR_CFG << ERR_error_string(ERR_get_error(), NULL);
426  EVP_CIPHER_CTX_free(ctx);
427  return secure_buffer();
428  }
429  DBG_CFG << "Extra length: " << extra_length;
430 
431  EVP_CIPHER_CTX_free(ctx);
432 
433  total_length = update_length+extra_length;
434  secure_buffer result;
435  for(int i = 0; i < total_length; i++)
436  {
437  result.push_back(plaintext_buffer[i]);
438  }
439 
440  DBG_CFG << "Successfully decrypted data to the value: " << utils::join(result, "");
441  DBG_CFG << "For a total decrypted length of: " << total_length;
442 
443  return result;
444 #else
445  size_t outWritten = 0;
446  secure_buffer result(encrypted.size(), '\0');
447 
448  CCCryptorStatus ccStatus = CCCrypt(kCCDecrypt,
449  kCCAlgorithmRC4,
450  kCCOptionPKCS7Padding,
451  key.data(),
452  key.size(),
453  nullptr,
454  encrypted.data(),
455  encrypted.size(),
456  result.data(),
457  result.size(),
458  &outWritten);
459 
460  assert(ccStatus == kCCSuccess);
461  assert(outWritten == encrypted.size());
462 
463  // the decrypted result is likely shorter than the encrypted data, so the extra padding needs to be removed.
464  while(!result.empty() && result.back() == 0) {
465  result.pop_back();
466  }
467 
468  return result;
469 #endif
470 }
471 
473 {
474  secure_buffer unescaped;
475  unescaped.reserve(text.size());
476  bool escaping = false;
477  for(char c : text) {
478  if(escaping) {
479  if(c == '\xa') {
480  unescaped.push_back('\xc');
481  } else if(c == '.') {
482  unescaped.push_back('@');
483  } else {
484  unescaped.push_back(c);
485  }
486  escaping = false;
487  } else if(c == '\x1') {
488  escaping = true;
489  } else {
490  unescaped.push_back(c);
491  }
492  }
493  assert(!escaping);
494  return unescaped;
495 }
496 
498 {
499  secure_buffer escaped;
500  escaped.reserve(text.size());
501  for(char c : text) {
502  if(c == '\x1') {
503  escaped.push_back('\x1');
504  escaped.push_back('\x1');
505  } else if(c == '\xc') {
506  escaped.push_back('\x1');
507  escaped.push_back('\xa');
508  } else if(c == '@') {
509  escaped.push_back('\x1');
510  escaped.push_back('.');
511  } else {
512  escaped.push_back(c);
513  }
514  }
515  return escaped;
516 }
secure_buffer(T &&... args)
Definition: credentials.cpp:47
#define ERR_CFG
Definition: credentials.cpp:42
static const unsigned char CREDENTIAL_SEPARATOR
Definition: credentials.cpp:75
static std::string get_system_username()
Definition: credentials.cpp:83
static const std::string EMPTY_LOGIN
#define DBG_CFG
Definition: credentials.cpp:41
static std::vector< login_info > credentials
Definition: credentials.cpp:72
static secure_buffer unescape(const secure_buffer &text)
static secure_buffer escape(const secure_buffer &text)
static secure_buffer aes_encrypt(const secure_buffer &text, const secure_buffer &key)
Encrypts the value of plaintext using key and a hard coded IV using AES.
static secure_buffer build_key(const std::string &server, const std::string &login)
Fills a secure_buffer with 32 bytes of deterministically generated bytes, then overwrites it with the...
static secure_buffer aes_decrypt(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 ...
static lg::log_domain log_config("config")
static void clear_credentials()
Declarations for File-IO.
std::size_t i
Definition: function.cpp:968
Standard logging facilities (interface).
void fill(const SDL_Rect &rect, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
Fill an area with the given colour.
Definition: draw.cpp:50
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
bool delete_file(const std::string &filename)
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:318
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode, bool create_directory)
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:50
std::string get_credentials_file()
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:51
Modify, read and display user preferences.
void set_remember_password(bool remember)
std::string password(const std::string &server, const std::string &login)
void save_credentials()
void set(const std::string &key, bool value)
Definition: general.cpp:165
bool remember_password()
void load_credentials()
std::string get(const std::string &key)
Definition: general.cpp:213
void set_login(const std::string &login)
std::string login()
void set_password(const std::string &server, const std::string &login, const std::string &key)
static std::string at(const std::string &file, int line)
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
@ REMOVE_EMPTY
void trim(std::string_view &s)
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
std::vector< std::string > split(const config_attribute_value &val)
std::string_view data
Definition: picture.cpp:194
An exception object used when an IO error occurs.
Definition: filesystem.hpp:64
std::size_t size() const
Definition: credentials.cpp:66
login_info(const std::string &username, const std::string &server)
Definition: credentials.cpp:63
secure_buffer key
Definition: credentials.cpp:59
std::string username
Definition: credentials.cpp:58
login_info(const std::string &username, const std::string &server, const secure_buffer &key)
Definition: credentials.cpp:60
std::string server
Definition: credentials.cpp:58
mock_char c