The Battle for Wesnoth  1.17.0-dev
credentials.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2017 - 2021
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/rc4.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 ERR_CFG LOG_STREAM(err , log_config)
41 
42 class secure_buffer : public std::vector<unsigned char>
43 {
44 public:
45  template<typename... T> secure_buffer(T&&... args)
46  : vector<unsigned char>(std::forward<T>(args)...)
47  {}
49  {
50  std::fill(begin(), end(), '\0');
51  }
52 };
53 
54 struct login_info
55 {
56  std::string username, server;
58  login_info(const std::string& username, const std::string& server, const secure_buffer& key)
59  : username(username), server(server), key(key)
60  {}
61  login_info(const std::string& username, const std::string& server)
62  : username(username), server(server), key()
63  {}
64  std::size_t size() const
65  {
66  return 3 + username.size() + server.size() + key.size();
67  }
68 };
69 
70 static std::vector<login_info> credentials;
71 
72 // Separate password entries with formfeed
73 static const unsigned char CREDENTIAL_SEPARATOR = '\f';
74 
75 static secure_buffer encrypt(const secure_buffer& text, const secure_buffer& key);
76 static secure_buffer decrypt(const secure_buffer& text, const secure_buffer& key);
77 static secure_buffer build_key(const std::string& server, const std::string& login);
78 static secure_buffer escape(const secure_buffer& text);
79 static secure_buffer unescape(const secure_buffer& text);
80 
81 static std::string get_system_username()
82 {
83  std::string res;
84 #ifdef _WIN32
85  wchar_t buffer[300];
86  DWORD size = 300;
87  if(GetUserNameW(buffer, &size)) {
88  //size includes a terminating null character.
89  assert(size > 0);
90  res = unicode_cast<std::string>(boost::iterator_range<wchar_t*>(buffer, buffer + size - 1));
91  }
92 #else
93  if(char* const login = getenv("USER")) {
94  res = login;
95  }
96 #endif
97  return res;
98 }
99 
100 static void clear_credentials()
101 {
102  // Zero them before clearing.
103  // Probably overly paranoid, but doesn't hurt?
104  for(auto& cred : credentials) {
105  std::fill(cred.username.begin(), cred.username.end(), '\0');
106  std::fill(cred.server.begin(), cred.server.end(), '\0');
107  }
108  credentials.clear();
109 }
110 
111 static const std::string EMPTY_LOGIN = "@@";
112 
113 namespace preferences
114 {
115  std::string login()
116  {
117  std::string name = preferences::get("login", EMPTY_LOGIN);
118  if(name == EMPTY_LOGIN) {
119  name = get_system_username();
120  } else if(name.size() > 2 && name.front() == '@' && name.back() == '@') {
121  name = name.substr(1, name.size() - 2);
122  } else {
123  ERR_CFG << "malformed user credentials (did you manually edit the preferences file?)" << std::endl;
124  }
125  if(name.empty()) {
126  return "player";
127  }
128  return name;
129  }
130 
131  void set_login(const std::string& login)
132  {
133  auto login_clean = login;
134  boost::trim(login_clean);
135 
136  preferences::set("login", '@' + login_clean + '@');
137  }
138 
140  {
141  return preferences::get("remember_password", false);
142  }
143 
144  void set_remember_password(bool remember)
145  {
146  preferences::set("remember_password", remember);
147 
148  if(remember) {
150  } else {
152  }
153  }
154 
155  std::string password(const std::string& server, const std::string& login)
156  {
157  auto login_clean = login;
158  boost::trim(login_clean);
159 
160  if(!remember_password()) {
161  if(!credentials.empty() && credentials[0].username == login_clean && credentials[0].server == server) {
162  auto temp = decrypt(credentials[0].key, build_key(server, login_clean));
163  return std::string(temp.begin(), temp.end());
164  } else {
165  return "";
166  }
167  }
168  auto cred = std::find_if(credentials.begin(), credentials.end(), [&](const login_info& cred) {
169  return cred.server == server && cred.username == login_clean;
170  });
171  if(cred == credentials.end()) {
172  return "";
173  }
174  auto temp = decrypt(cred->key, build_key(server, login_clean));
175  return std::string(temp.begin(), temp.end());
176  }
177 
178  void set_password(const std::string& server, const std::string& login, const std::string& key)
179  {
180  auto login_clean = login;
181  boost::trim(login_clean);
182 
183  secure_buffer temp(key.begin(), key.end());
184  if(!remember_password()) {
186  credentials.emplace_back(login_clean, server, encrypt(temp, build_key(server, login_clean)));
187  return;
188  }
189  auto cred = std::find_if(credentials.begin(), credentials.end(), [&](const login_info& cred) {
190  return cred.server == server && cred.username == login_clean;
191  });
192  if(cred == credentials.end()) {
193  // This is equivalent to emplace_back, but also returns the iterator to the new element
194  cred = credentials.emplace(credentials.end(), login_clean, server);
195  }
196  cred->key = encrypt(temp, build_key(server, login_clean));
197  }
198 
200  {
201  if(!remember_password()) {
202  return;
203  }
205  std::string cred_file = filesystem::get_credentials_file();
206  if(!filesystem::file_exists(cred_file)) {
207  return;
208  }
209  filesystem::scoped_istream stream = filesystem::istream_file(cred_file, false);
210  // Credentials file is a binary blob, so use streambuf iterator
211  secure_buffer data((std::istreambuf_iterator<char>(*stream)), (std::istreambuf_iterator<char>()));
212  data = decrypt(data, build_key("global", get_system_username()));
213  if(data.empty() || data[0] != CREDENTIAL_SEPARATOR) {
214  ERR_CFG << "Invalid data in credentials file\n";
215  return;
216  }
217  for(const std::string& elem : utils::split(std::string(data.begin(), data.end()), CREDENTIAL_SEPARATOR, utils::REMOVE_EMPTY)) {
218  std::size_t at = elem.find_last_of('@');
219  std::size_t eq = elem.find_first_of('=', at + 1);
220  if(at != std::string::npos && eq != std::string::npos) {
221  secure_buffer key(elem.begin() + eq + 1, elem.end());
222  credentials.emplace_back(elem.substr(0, at), elem.substr(at + 1, eq - at - 1), unescape(key));
223  }
224  }
225  }
226 
228  {
229  if(!remember_password()) {
231  return;
232  }
233  secure_buffer credentials_data(1, CREDENTIAL_SEPARATOR);
234  std::size_t offset = 1;
235  for(const auto& cred : credentials) {
236  credentials_data.resize(credentials_data.size() + cred.size(), CREDENTIAL_SEPARATOR);
237  std::copy(cred.username.begin(), cred.username.end(), credentials_data.begin() + offset);
238  offset += cred.username.size();
239  credentials_data[offset++] = '@';
240  std::copy(cred.server.begin(), cred.server.end(), credentials_data.begin() + offset);
241  offset += cred.server.size();
242  credentials_data[offset++] = '=';
243  secure_buffer key_escaped = escape(cred.key);
244  // Escaping may increase the length, so resize again if so
245  credentials_data.resize(credentials_data.size() + key_escaped.size() - cred.key.size());
246  std::copy(key_escaped.begin(), key_escaped.end(), credentials_data.begin() + offset);
247  offset += key_escaped.size() + 1;
248  }
249  try {
251  secure_buffer encrypted = encrypt(credentials_data, build_key("global", get_system_username()));
252  credentials_file->write(reinterpret_cast<const char*>(encrypted.data()), encrypted.size());
253  } catch(const filesystem::io_exception&) {
254  ERR_CFG << "error writing to credentials file '" << filesystem::get_credentials_file() << "'" << std::endl;
255  }
256  }
257 }
258 
259 // TODO: Key-stretching (bcrypt was recommended)
260 secure_buffer build_key(const std::string& server, const std::string& login)
261 {
262  std::string sysname = get_system_username();
263  secure_buffer result(std::max<std::size_t>(server.size() + login.size() + sysname.size(), 32));
264  unsigned char i = 0;
265  std::generate(result.begin(), result.end(), [&i]() {return 'x' ^ i++;});
266  std::copy(login.begin(), login.end(), result.begin());
267  std::copy(sysname.begin(), sysname.end(), result.begin() + login.size());
268  std::copy(server.begin(), server.end(), result.begin() + login.size() + sysname.size());
269  return result;
270 }
271 
272 static secure_buffer rc4_crypt(const secure_buffer& text, const secure_buffer& key)
273 {
274  secure_buffer result(text.size(), '\0');
275 #ifndef __APPLE__
276  RC4_KEY cipher_key;
277  RC4_set_key(&cipher_key, key.size(), key.data());
278  const std::size_t block_size = key.size();
279  const std::size_t blocks = text.size() / block_size;
280  const std::size_t extra = text.size() % block_size;
281  for(std::size_t i = 0; i < blocks * block_size; i += block_size) {
282  RC4(&cipher_key, block_size, text.data() + i, result.data() + i);
283  }
284  if(extra) {
285  std::size_t i = blocks * block_size;
286  RC4(&cipher_key, extra, text.data() + i, result.data() + i);
287  }
288 #else
289  size_t outWritten = 0;
290  CCCryptorStatus ccStatus = CCCrypt(kCCDecrypt,
291  kCCAlgorithmRC4,
292  kCCOptionPKCS7Padding,
293  key.data(),
294  key.size(),
295  nullptr,
296  text.data(),
297  text.size(),
298  result.data(),
299  result.size(),
300  &outWritten);
301 
302  assert(ccStatus == kCCSuccess);
303  assert(outWritten == text.size());
304 #endif
305  return result;
306 }
307 
309 {
310  return rc4_crypt(text, key);
311 }
312 
314 {
315  auto buf = rc4_crypt(text, key);
316  while(!buf.empty() && buf.back() == 0) {
317  buf.pop_back();
318  }
319  return buf;
320 }
321 
323 {
324  secure_buffer unescaped;
325  unescaped.reserve(text.size());
326  bool escaping = false;
327  for(char c : text) {
328  if(escaping) {
329  if(c == '\xa') {
330  unescaped.push_back('\xc');
331  } else if(c == '.') {
332  unescaped.push_back('@');
333  } else {
334  unescaped.push_back(c);
335  }
336  escaping = false;
337  } else if(c == '\x1') {
338  escaping = true;
339  } else {
340  unescaped.push_back(c);
341  }
342  }
343  assert(!escaping);
344  return unescaped;
345 }
346 
348 {
349  secure_buffer escaped;
350  escaped.reserve(text.size());
351  for(char c : text) {
352  if(c == '\x1') {
353  escaped.push_back('\x1');
354  escaped.push_back('\x1');
355  } else if(c == '\xc') {
356  escaped.push_back('\x1');
357  escaped.push_back('\xa');
358  } else if(c == '@') {
359  escaped.push_back('\x1');
360  escaped.push_back('.');
361  } else {
362  escaped.push_back(c);
363  }
364  }
365  return escaped;
366 }
secure_buffer key
Definition: credentials.cpp:57
login_info(const std::string &username, const std::string &server)
Definition: credentials.cpp:61
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)
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:161
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:56
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:45
std::string get(const std::string &key)
Definition: general.cpp:209
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:73
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:40
static std::string get_system_username()
Definition: credentials.cpp:81
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:70
#define ERR_CFG
Definition: credentials.cpp:40
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:64
login_info(const std::string &username, const std::string &server, const secure_buffer &key)
Definition: credentials.cpp:58
static secure_buffer build_key(const std::string &server, const std::string &login)
mock_char c