The Battle for Wesnoth  1.17.4+dev
credentials.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2017 - 2022
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 #else
31 #include <CommonCrypto/CommonCryptor.h>
32 #endif
33 
34 #ifdef _WIN32
35 #include <boost/range/iterator_range.hpp>
36 #include <windows.h>
37 #endif
38 
39 static lg::log_domain log_config("config");
40 #define DBG_CFG LOG_STREAM(debug , log_config)
41 #define ERR_CFG LOG_STREAM(err , log_config)
42 
43 class secure_buffer : public std::vector<unsigned char>
44 {
45 public:
46  template<typename... T> secure_buffer(T&&... args)
47  : vector<unsigned char>(std::forward<T>(args)...)
48  {}
50  {
51  std::fill(begin(), end(), '\0');
52  }
53 };
54 
55 struct login_info
56 {
57  std::string username, server;
59  login_info(const std::string& username, const std::string& server, const secure_buffer& key)
60  : username(username), server(server), key(key)
61  {}
62  login_info(const std::string& username, const std::string& server)
63  : username(username), server(server), key()
64  {}
65  std::size_t size() const
66  {
67  return 3 + username.size() + server.size() + key.size();
68  }
69 };
70 
71 static std::vector<login_info> credentials;
72 
73 // Separate password entries with formfeed
74 static const unsigned char CREDENTIAL_SEPARATOR = '\f';
75 
76 static secure_buffer encrypt(const secure_buffer& text, const secure_buffer& key);
77 static secure_buffer decrypt(const secure_buffer& text, const secure_buffer& key);
78 static secure_buffer build_key(const std::string& server, const std::string& login);
79 static secure_buffer escape(const secure_buffer& text);
80 static secure_buffer unescape(const secure_buffer& text);
81 
82 static std::string get_system_username()
83 {
84  std::string res;
85 #ifdef _WIN32
86  wchar_t buffer[300];
87  DWORD size = 300;
88  if(GetUserNameW(buffer, &size)) {
89  //size includes a terminating null character.
90  assert(size > 0);
91  res = unicode_cast<std::string>(boost::iterator_range<wchar_t*>(buffer, buffer + size - 1));
92  }
93 #else
94  if(char* const login = getenv("USER")) {
95  res = login;
96  }
97 #endif
98  return res;
99 }
100 
101 static void clear_credentials()
102 {
103  // Zero them before clearing.
104  // Probably overly paranoid, but doesn't hurt?
105  for(auto& cred : credentials) {
106  std::fill(cred.username.begin(), cred.username.end(), '\0');
107  std::fill(cred.server.begin(), cred.server.end(), '\0');
108  }
109  credentials.clear();
110 }
111 
112 static const std::string EMPTY_LOGIN = "@@";
113 
114 namespace preferences
115 {
116  std::string login()
117  {
118  std::string name = preferences::get("login", EMPTY_LOGIN);
119  if(name == EMPTY_LOGIN) {
120  name = get_system_username();
121  } else if(name.size() > 2 && name.front() == '@' && name.back() == '@') {
122  name = name.substr(1, name.size() - 2);
123  } else {
124  ERR_CFG << "malformed user credentials (did you manually edit the preferences file?)" << std::endl;
125  }
126  if(name.empty()) {
127  return "player";
128  }
129  return name;
130  }
131 
132  void set_login(const std::string& login)
133  {
134  auto login_clean = login;
135  boost::trim(login_clean);
136 
137  preferences::set("login", '@' + login_clean + '@');
138  }
139 
141  {
142  return preferences::get("remember_password", false);
143  }
144 
145  void set_remember_password(bool remember)
146  {
147  preferences::set("remember_password", remember);
148 
149  if(remember) {
151  } else {
153  }
154  }
155 
156  std::string password(const std::string& server, const std::string& login)
157  {
158  DBG_CFG << "Retrieving password for server: '" << server << "', login: '" << login << "'\n";
159  auto login_clean = login;
160  boost::trim(login_clean);
161 
162  if(!remember_password()) {
163  if(!credentials.empty() && credentials[0].username == login_clean && credentials[0].server == server) {
164  auto temp = decrypt(credentials[0].key, build_key(server, login_clean));
165  return std::string(temp.begin(), temp.end());
166  } else {
167  return "";
168  }
169  }
170  auto cred = std::find_if(credentials.begin(), credentials.end(), [&](const login_info& cred) {
171  return cred.server == server && cred.username == login_clean;
172  });
173  if(cred == credentials.end()) {
174  return "";
175  }
176  auto temp = decrypt(cred->key, build_key(server, login_clean));
177  return std::string(temp.begin(), temp.end());
178  }
179 
180  void set_password(const std::string& server, const std::string& login, const std::string& key)
181  {
182  DBG_CFG << "Setting password for server: '" << server << "', login: '" << login << "'\n";
183  auto login_clean = login;
184  boost::trim(login_clean);
185 
186  secure_buffer temp(key.begin(), key.end());
187  if(!remember_password()) {
189  credentials.emplace_back(login_clean, server, encrypt(temp, build_key(server, login_clean)));
190  return;
191  }
192  auto cred = std::find_if(credentials.begin(), credentials.end(), [&](const login_info& cred) {
193  return cred.server == server && cred.username == login_clean;
194  });
195  if(cred == credentials.end()) {
196  // This is equivalent to emplace_back, but also returns the iterator to the new element
197  cred = credentials.emplace(credentials.end(), login_clean, server);
198  }
199  cred->key = encrypt(temp, build_key(server, login_clean));
200  }
201 
203  {
204  if(!remember_password()) {
205  return;
206  }
208  std::string cred_file = filesystem::get_credentials_file();
209  if(!filesystem::file_exists(cred_file)) {
210  return;
211  }
212  filesystem::scoped_istream stream = filesystem::istream_file(cred_file, false);
213  // Credentials file is a binary blob, so use streambuf iterator
214  secure_buffer data((std::istreambuf_iterator<char>(*stream)), (std::istreambuf_iterator<char>()));
215  data = decrypt(data, build_key("global", get_system_username()));
216  if(data.empty() || data[0] != CREDENTIAL_SEPARATOR) {
217  ERR_CFG << "Invalid data in credentials file\n";
218  return;
219  }
220  for(const std::string& elem : utils::split(std::string(data.begin(), data.end()), CREDENTIAL_SEPARATOR, utils::REMOVE_EMPTY)) {
221  std::size_t at = elem.find_last_of('@');
222  std::size_t eq = elem.find_first_of('=', at + 1);
223  if(at != std::string::npos && eq != std::string::npos) {
224  secure_buffer key(elem.begin() + eq + 1, elem.end());
225  credentials.emplace_back(elem.substr(0, at), elem.substr(at + 1, eq - at - 1), unescape(key));
226  }
227  }
228  }
229 
231  {
232  if(!remember_password()) {
234  return;
235  }
236  secure_buffer credentials_data;
237  for(const auto& cred : credentials) {
238  credentials_data.push_back(CREDENTIAL_SEPARATOR);
239  credentials_data.insert(credentials_data.end(), cred.username.begin(), cred.username.end());
240  credentials_data.push_back('@');
241  credentials_data.insert(credentials_data.end(), cred.server.begin(), cred.server.end());
242  credentials_data.push_back('=');
243  secure_buffer key_escaped = escape(cred.key);
244  credentials_data.insert(credentials_data.end(), key_escaped.begin(), key_escaped.end());
245  }
246  try {
248  secure_buffer encrypted = encrypt(credentials_data, build_key("global", get_system_username()));
249  credentials_file->write(reinterpret_cast<const char*>(encrypted.data()), encrypted.size());
250  } catch(const filesystem::io_exception&) {
251  ERR_CFG << "error writing to credentials file '" << filesystem::get_credentials_file() << "'" << std::endl;
252  }
253  }
254 }
255 
256 // TODO: Key-stretching (bcrypt was recommended)
257 secure_buffer build_key(const std::string& server, const std::string& login)
258 {
259  std::string sysname = get_system_username();
260  secure_buffer result(std::max<std::size_t>(server.size() + login.size() + sysname.size(), 32));
261  unsigned char i = 0;
262  std::generate(result.begin(), result.end(), [&i]() {return 'x' ^ i++;});
263  std::copy(login.begin(), login.end(), result.begin());
264  std::copy(sysname.begin(), sysname.end(), result.begin() + login.size());
265  std::copy(server.begin(), server.end(), result.begin() + login.size() + sysname.size());
266  return result;
267 }
268 
269 static secure_buffer rc4_crypt(const secure_buffer& text, const secure_buffer& key)
270 {
271  secure_buffer result(text.size(), '\0');
272 #ifndef __APPLE__
273  int outlen;
274  int tmplen;
275 
276  EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
277 
278  // TODO: use EVP_EncryptInit_ex2 once openssl 3.0 is more widespread
279  EVP_EncryptInit_ex(ctx, EVP_rc4(), NULL, key.data(), NULL);
280  EVP_EncryptUpdate(ctx, result.data(), &outlen, text.data(), text.size());
281  EVP_EncryptFinal_ex(ctx, result.data() + outlen, &tmplen);
282 
283  EVP_CIPHER_CTX_free(ctx);
284 #else
285  size_t outWritten = 0;
286  CCCryptorStatus ccStatus = CCCrypt(kCCDecrypt,
287  kCCAlgorithmRC4,
288  kCCOptionPKCS7Padding,
289  key.data(),
290  key.size(),
291  nullptr,
292  text.data(),
293  text.size(),
294  result.data(),
295  result.size(),
296  &outWritten);
297 
298  assert(ccStatus == kCCSuccess);
299  assert(outWritten == text.size());
300 #endif
301  return result;
302 }
303 
305 {
306  return rc4_crypt(text, key);
307 }
308 
310 {
311  auto buf = rc4_crypt(text, key);
312  while(!buf.empty() && buf.back() == 0) {
313  buf.pop_back();
314  }
315  return buf;
316 }
317 
319 {
320  secure_buffer unescaped;
321  unescaped.reserve(text.size());
322  bool escaping = false;
323  for(char c : text) {
324  if(escaping) {
325  if(c == '\xa') {
326  unescaped.push_back('\xc');
327  } else if(c == '.') {
328  unescaped.push_back('@');
329  } else {
330  unescaped.push_back(c);
331  }
332  escaping = false;
333  } else if(c == '\x1') {
334  escaping = true;
335  } else {
336  unescaped.push_back(c);
337  }
338  }
339  assert(!escaping);
340  return unescaped;
341 }
342 
344 {
345  secure_buffer escaped;
346  escaped.reserve(text.size());
347  for(char c : text) {
348  if(c == '\x1') {
349  escaped.push_back('\x1');
350  escaped.push_back('\x1');
351  } else if(c == '\xc') {
352  escaped.push_back('\x1');
353  escaped.push_back('\xa');
354  } else if(c == '@') {
355  escaped.push_back('\x1');
356  escaped.push_back('.');
357  } else {
358  escaped.push_back(c);
359  }
360  }
361  return escaped;
362 }
secure_buffer key
Definition: credentials.cpp:58
login_info(const std::string &username, const std::string &server)
Definition: credentials.cpp:62
static secure_buffer rc4_crypt(const secure_buffer &text, const secure_buffer &key)
bool remember_password()
bool delete_file(const std::string &filename)
Definition: filesystem.cpp:987
static secure_buffer encrypt(const secure_buffer &text, const secure_buffer &key)
void set_remember_password(bool remember)
#define DBG_CFG
Definition: credentials.cpp:40
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:263
ucs4_convert_impl::enableif< TD, typename TS::value_type >::type unicode_cast(const TS &source)
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
static secure_buffer unescape(const secure_buffer &text)
STL namespace.
void set(const std::string &key, bool value)
Definition: general.cpp:167
void save_credentials()
static lg::log_domain log_config("config")
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode, bool create_directory)
std::string username
Definition: credentials.cpp:57
static secure_buffer escape(const secure_buffer &text)
static std::string at(const std::string &file, int line)
secure_buffer(T &&... args)
Definition: credentials.cpp:46
std::string get(const std::string &key)
Definition: general.cpp:215
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:87
static void clear_credentials()
void set_login(const std::string &login)
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:39
void set_password(const std::string &server, const std::string &login, const std::string &key)
Modify, read and display user preferences.
std::string login()
static const unsigned char CREDENTIAL_SEPARATOR
Definition: credentials.cpp:74
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:40
static std::string get_system_username()
Definition: credentials.cpp:82
std::size_t i
Definition: function.cpp:967
static secure_buffer decrypt(const secure_buffer &text, const secure_buffer &key)
An exception object used when an IO error occurs.
Definition: filesystem.hpp:48
static const std::string EMPTY_LOGIN
std::string password(const std::string &server, const std::string &login)
Declarations for File-IO.
static std::vector< login_info > credentials
Definition: credentials.cpp:71
#define ERR_CFG
Definition: credentials.cpp:41
std::vector< std::string > split(const config_attribute_value &val)
void load_credentials()
std::string get_credentials_file()
Standard logging facilities (interface).
void trim(std::string_view &s)
std::size_t size() const
Definition: credentials.cpp:65
login_info(const std::string &username, const std::string &server, const secure_buffer &key)
Definition: credentials.cpp:59
static secure_buffer build_key(const std::string &server, const std::string &login)
mock_char c