1 /*
2  * Save username and passwords details at login and allow selection from the login screen.
3  * Also store new character and successful password change details.
4  * http://www.eternal-lands.com/forum/index.php?/topic/61189-new-login-screen-for-multiple-characters/
5  *
6  * Author bluap/pjbroad March 2019.
7  *
8 */
9 
10 #include <algorithm>
11 #include <fstream>
12 #include <iostream>
13 #include <iterator>
14 #include <sstream>
15 #include <string>
16 #include <vector>
17 
18 #include "elloggingwrapper.h"
19 #include "elwindows.h"
20 #include "font.h"
21 #include "gl_init.h"
22 #include "loginwin.h"
23 #include "multiplayer.h"
24 #include "named_colours.h"
25 #include "io/elpathwrapper.h"
26 #include "password_manager.h"
27 #include "translate.h"
28 #include "sound.h"
29 #include "xor_cipher.hpp"
30 
31 extern "C"
32 {
33 	static int display_pm_handler(window_info *win);
34 	static int ui_scale_pm_handler(window_info *win);
35 	static int change_pm_font_handler(window_info *win, font_cat cat);
36 	static int click_pm_handler(window_info *win, int mx, int my, Uint32 flags);
37 	static int mouseover_pm_handler(window_info *win, int mx, int my);
38 	static int click_show_password(widget_list *w, int mx, int my, Uint32 flags);
39 }
40 
41 namespace Password_Manaager
42 {
43 	//	A simple class to hold a username and password pair
44 	//
45 	class Login
46 	{
47 		public:
Login(const std::string & the_name,const std::string & the_password)48 			Login(const std::string& the_name, const std::string& the_password):
49 				user_name(the_name), clear_password(the_password) {}
get_name(void) const50 			const std::string & get_name(void) const { return user_name; }
get_password(void) const51 			const std::string & get_password(void) const { return clear_password; }
52 			static bool sort_compare(const Login &a, const Login &b);
53 		private:
54 			std::string user_name, clear_password;
55 	};
56 
57 	// Used by the sort algorithm to alphabetically compare two Login names, case insensitive
58 	//
sort_compare(const Login & a,const Login & b)59 	bool Login::sort_compare(const Login &a, const Login &b)
60 	{
61 		std::string alower(a.get_name());
62 		std::transform(alower.begin(), alower.end(), alower.begin(), tolower);
63 		std::string blower(b.get_name());
64 		std::transform(blower.begin(), blower.end(), blower.begin(), tolower);
65 		return alower < blower;
66 	}
67 
68 	//	A simple class to storge and manage a collection of username/password pairs.
69 	//
70 	class Logins
71 	{
72 		public:
Logins(void)73 			Logins(void) :
74 				file_name(std::string(get_path_config()) + "passmngr_logins"),
75 				cipher(std::string(get_path_config()) + "passmngr_key", MAX_USERNAME_LENGTH)
76 				{ load(); }
77 			void load(void);
78 			void set_details(void) const;
79 			void add(const std::string& user_name, const std::string& password);
80 			static bool is_valid_password(const std::string &password);
81 			static bool get_validated_new_pasword(const char * old_and_new_password, std::string &new_password);
pending_change(const std::string & password)82 			void pending_change(const std::string& password) { pending_new_password = password; }
83 			bool confirm_change(void);
size(void) const84 			size_t size(void) const { return logins.size(); }
begin() const85 			std::vector<Login>::const_iterator begin() const { return logins.begin(); }
end() const86 			std::vector<Login>::const_iterator end() const { return logins.end(); }
87 		private:
88 			bool common_add(const std::string& user_name, const std::string& password);
89 			void save(void);
90 			std::string pending_new_password;
91 			std::vector<Login> logins;
92 			std::string file_name;
93 			XOR_Cipher::Cipher cipher;
94 	};
95 
96 	//	Add if the specified username/password are valid and new or modified.
97 	//	Return true if added.
98 	//
common_add(const std::string & user_name,const std::string & password)99 	bool Logins::common_add(const std::string& user_name, const std::string& password)
100 	{
101 		if ((user_name.size() >= MAX_USERNAME_LENGTH) || (password.size()  >= MAX_USERNAME_LENGTH))
102 		{
103 			LOG_ERROR("%s: Error adding username [%s]\n", __PRETTY_FUNCTION__, user_name.c_str());
104 			return false;
105 		}
106 		std::string lc_username = user_name;
107 		std::transform(lc_username.begin(), lc_username.end(), lc_username.begin(), tolower);
108 		for(std::vector<Login>::iterator i=logins.begin(); i<end(); ++i)
109 		{
110 			std::string temp_lower = i->get_name();
111 			std::transform(temp_lower.begin(), temp_lower.end(), temp_lower.begin(), ::tolower);
112 			if (temp_lower == lc_username)
113 			{
114 				if ((i->get_name() == user_name) && (i->get_password() == password))
115 					return false;
116 				logins.erase(i);
117 				break;
118 			}
119 		}
120 		logins.push_back(Login(user_name, password));
121 		return true;
122 	}
123 
124 	//	Add a single new username/password pair if valid and new, then save.  Not used when loading from file.
125 	//
add(const std::string & user_name,const std::string & password)126 	void Logins::add(const std::string& user_name, const std::string& password)
127 	{
128 		if (common_add(user_name, password))
129 		{
130 			std::sort(logins.begin(), logins.end(), Login::sort_compare);
131 			save();
132 		}
133 	}
134 
135 	//	Validates the string is a password, of the correct size and containing valid characters.
136 	//
is_valid_password(const std::string & password)137 	bool Logins::is_valid_password(const std::string &password)
138 	{
139 		if ((password.size() < MIN_PASSWORD_LEN) || (password.size() >= MAX_PASSWORD_LEN))
140 			return false;
141 		for (auto c: password)
142 			if (!VALID_PASSWORD_CHAR(c))
143 				return false;
144 		return true;
145 	}
146 
147 	//	Common function to validate the string contains two words, and that the second at least is a valid password
148 	//	Returns true if the second word is a valid password and returned in the provided string
149 	//
get_validated_new_pasword(const char * old_and_new_password,std::string & new_password)150 	bool Logins::get_validated_new_pasword(const char * old_and_new_password, std::string &new_password)
151 	{
152 		std::istringstream iss(old_and_new_password);
153 		std::vector<std::string> words((std::istream_iterator<std::string>(iss)), std::istream_iterator<std::string>());
154 		if ((words.size() == 2) && is_valid_password(words[1]))
155 		{
156 			new_password = words[1];
157 			return true;
158 		}
159 		else
160 			return false;
161 	}
162 
163 	//	Called when the server sends the change password confirmation.  Save the new password.
164 	//
confirm_change(void)165 	bool Logins::confirm_change(void)
166 	{
167 		bool ret_value;
168 		if ((ret_value = is_valid_password(pending_new_password)))
169 		{
170 			add(get_username(), pending_new_password);
171 			set_password(pending_new_password.c_str());
172 		}
173 		pending_new_password.clear();
174 		return ret_value;
175 	}
176 
177 	//	Set the current password assiociated with the current username.
178 	//
set_details(void) const179 	void Logins::set_details(void) const
180 	{
181 		std::string user_name = std::string(get_username());
182 		set_username(get_username());  // set all versions of the username
183 		for(std::vector<Login>::const_iterator i=begin(); i<end(); ++i)
184 		{
185 			if (i->get_name() == user_name)
186 			{
187 				set_password(i->get_password().c_str());
188 				return;
189 			}
190 		}
191 	}
192 
193 	// Load username/password pairs from the file in the current config directory.
194 	//
load(void)195 	void Logins::load(void)
196 	{
197 		logins.clear();
198 		std::ifstream in(file_name.c_str());
199 		if (!in)
200 		{
201 			LOG_ERROR("%s: Failed to open [%s]\n", __PRETTY_FUNCTION__, file_name.c_str());
202 			return;
203 		}
204 		std::string line;
205 		while (getline(in, line))
206 		{
207 			std::istringstream iss(line);
208 			std::vector<std::string> words((std::istream_iterator<std::string>(iss)), std::istream_iterator<std::string>());
209 			if ((words.size() == 2))
210 			{
211 				if (!common_add(words[0], cipher.decrypt(cipher.hex_to_cipher(words[1]))))
212 					LOG_ERROR("%s: Not loading username [%s]\n", __PRETTY_FUNCTION__, words[0].c_str());
213 			}
214 			else
215 				LOG_ERROR("%s: Invalid line [%s]\n", __PRETTY_FUNCTION__, line.c_str());
216 		}
217 		std::sort(logins.begin(), logins.end(), Login::sort_compare);
218 		in.close();
219 	}
220 
221 	// Save username/password pairs to the file in the current config directory.
222 	//
save(void)223 	void Logins::save(void)
224 	{
225 		std::ofstream out(file_name.c_str(), std::ios_base::out | std::ios_base::trunc);
226 		if (out)
227 		{
228 			for(std::vector<Login>::const_iterator i=begin(); i<end(); ++i)
229 				out << i->get_name() << " " << cipher.cipher_to_hex(cipher.encrypt(i->get_password())) << std::endl;
230 			out.close();
231 		}
232 		else
233 			LOG_ERROR("%s: Failed to write [%s]\n", __PRETTY_FUNCTION__, file_name.c_str());
234 	}
235 
236 	//	A simple class to manage the username list section window.
237 	//
238 	class Window
239 	{
240 		public:
Window()241 			Window() : window_id(-1), checkbox_id(0), checkbox_label_id(0), show_passwords(0), mouse_over_line(-1) {}
~Window()242 			~Window() { destroy(); }
243 			void open(void);
244 			int display(Logins *logins, window_info *win);
245 			int click(Logins *logins, window_info *win, int mx, int my, Uint32 flags);
246 			int mouseover(Logins *logins, window_info *win, int mx, int my);
247 			int ui_scale(Logins *logins, window_info *win);
248 			int change_font(Logins *logins, window_info *win, eternal_lands::FontManager::Category cat);
249 			void toggle_show_password(Logins *logins, widget_list *w);
resize(Logins * logins)250 			void resize(Logins *logins) { ui_scale(logins, &windows_list.window[window_id]); }
251 		private:
252 			void destroy(void);
253 			int border_x, border_y, username_sep_y;
254 			int window_id;
255 			Uint32 scroll_id, checkbox_id, checkbox_label_id;
256 			int show_passwords;
257 			int mouse_over_line;
258 			size_t max_displayed;
259 	};
260 
261 	//  Set and update the list window scale.
262 	//
ui_scale(Logins * logins,window_info * win)263 	int Window::ui_scale(Logins *logins, window_info *win)
264 	{
265 		border_x = border_y = username_sep_y = (int)(0.5 + win->current_scale * 10);
266 		size_t max_available = static_cast<size_t>(0.8 * window_height - 2 * border_y + username_sep_y) / (username_sep_y + win->default_font_len_y);
267 		max_displayed = std::min(max_available, logins->size());
268 		int height = 2 * border_y + max_displayed * win->default_font_len_y + (max_displayed - 1) * username_sep_y;
269 		int width = 2 * border_x + win->default_font_max_len_x * (MAX_USERNAME_LENGTH - 1) + win->box_size;
270 		if (show_passwords)
271 			width += border_x + win->default_font_max_len_x * (MAX_USERNAME_LENGTH - 1);
272 		height = std::max(height, 4 * win->box_size);
273 
274 		int y_box, y_label;
275 		if (checkbox_id > 0)
276 			widget_destroy(win->window_id, checkbox_id);
277 		if (checkbox_label_id > 0)
278 			widget_destroy(win->window_id, checkbox_label_id);
279 		if (win->box_size >= win->default_font_len_y)
280 		{
281 			y_box = height;
282 			y_label = height + (win->box_size - win->default_font_len_y) / 2;
283 		}
284 		else
285 		{
286 			y_box = height + (win->default_font_len_y - win->box_size) / 2;
287 			y_label = height;
288 		}
289 		checkbox_id = checkbox_add_extended(win->window_id, 2, NULL, border_x, y_box,
290 			win->box_size, win->box_size, 0, win->current_scale, &show_passwords);
291 		checkbox_label_id = label_add_extended(window_id, 3, NULL, 2 * border_x + win->box_size, y_label, 0, win->current_scale, show_passwords_str);
292 		widget_set_OnClick(window_id, checkbox_id, (int (*)())&click_show_password);
293 		widget_set_OnClick(window_id, checkbox_label_id, (int (*)())&click_show_password);
294 		height += std::max(win->box_size, win->default_font_len_y) + border_y;
295 		width = std::max(width, 4 * border_x + 2 * win->box_size + widget_get_width(win->window_id, checkbox_label_id));
296 
297 		widget_resize(win->window_id, scroll_id, win->box_size, height - win->box_size);
298 		widget_move(win->window_id, scroll_id, width - win->box_size, win->box_size);
299 		vscrollbar_set_bar_len(win->window_id, scroll_id, ((logins->size() < max_displayed) ?0: logins->size() - max_displayed));
300 
301 		resize_window(window_id, width, height);
302 		move_window(window_id, win->pos_id, win->pos_loc, window_width / 2 + ((window_width / 2 - width) / 2), (window_height - height) / 2);
303 		return 1;
304 	}
305 
change_font(Logins * logins,window_info * win,eternal_lands::FontManager::Category cat)306 	int Window::change_font(Logins *logins, window_info *win, eternal_lands::FontManager::Category cat)
307 	{
308 		if (cat != win->font_category)
309 			return 0;
310 		ui_scale(logins, win);
311 		return 1;
312 	}
313 
314 	//	Handle if the show password checkbox or label are clicked
315 	//
toggle_show_password(Logins * logins,widget_list * w)316 	void Window::toggle_show_password(Logins *logins, widget_list *w)
317 	{
318 		if (w->id == checkbox_label_id)
319 		{
320 			do_click_sound();
321 			show_passwords ^= 1;
322 		}
323 		ui_scale(logins, &windows_list.window[window_id]);
324 	}
325 
326 	//	Handle mouse over events, highlighting the username under the mouse.
327 	//
mouseover(Logins * logins,window_info * win,int mx,int my)328 	int Window::mouseover(Logins *logins, window_info *win, int mx, int my)
329 	{
330 		if ((my > border_y) && (my < win->len_y - border_y))
331 		{
332 			if ((mx > border_x) && (mx < (win->len_x - win->box_size - border_x)))
333 			{
334 				mouse_over_line = (my - border_y) / (username_sep_y + win->default_font_len_y);
335 				return 1;
336 			}
337 		}
338 		return 0;
339 	}
340 
341 	//  Response to clicking on a username or scrolling the list window.
342 	//
click(Logins * logins,window_info * win,int mx,int my,Uint32 flags)343 	int Window::click(Logins *logins, window_info *win, int mx, int my, Uint32 flags)
344 	{
345 		if (flags & ELW_WHEEL_UP)
346 		{
347 			vscrollbar_scroll_up(win->window_id, scroll_id);
348 			return 1;
349 		}
350 		else if (flags & ELW_WHEEL_DOWN)
351 		{
352 			vscrollbar_scroll_down(win->window_id, scroll_id);
353 			return 1;
354 		}
355 		else if (((flags & ELW_LEFT_MOUSE) || (flags & ELW_RIGHT_MOUSE)) && (my > border_y) && (my < win->len_y - border_y))
356 		{
357 			if ((mx > border_x) && (mx < (win->len_x - win->box_size - border_x)))
358 			{
359 				size_t over_item = vscrollbar_get_pos(win->window_id, scroll_id) + (my - border_y) / (username_sep_y + win->default_font_len_y);
360 				if (over_item < logins->size())
361 				{
362 					set_username((logins->begin() + over_item)->get_name().c_str());
363 					set_password((logins->begin() + over_item)->get_password().c_str());
364 					do_click_sound();
365 					hide_window(window_id);
366 					if (flags & ELW_LEFT_MOUSE)
367 						send_login_info();
368 					return 1;
369 				}
370 			}
371 		}
372 		return 0;
373 	}
374 
375 	//	Dispalay the username list window.
376 	//
display(Logins * logins,window_info * win)377 	int Window::display(Logins *logins, window_info *win)
378 	{
379 		if (!passmngr_enabled)
380 		{
381 			hide_window(window_id);
382 			return 1;
383 		}
384 		int y = 0;
385 		size_t scroll_off = vscrollbar_get_pos(win->window_id, scroll_id);
386 		for (size_t i = scroll_off; i < (scroll_off + max_displayed); ++i)
387 		{
388 			if (i >= logins->size())
389 				break;
390 			std::vector<Login>::const_iterator curr = logins->begin() + i;
391 			if (i == static_cast<size_t>(scroll_off + mouse_over_line))
392 				elglColourN("global.mousehighlight");
393 			else
394 				glColor3f(1.0f, 1.0f, 1.0f);
395 			draw_string_zoomed (border_x, border_y + y, (const unsigned char*)curr->get_name().c_str(), 1, win->current_scale);
396 			if (show_passwords)
397 				draw_string_zoomed (2 * border_x + win->default_font_max_len_x * (MAX_USERNAME_LENGTH - 1), border_y + y, (const unsigned char*)curr->get_password().c_str(), 1, win->current_scale);
398 			y += win->default_font_len_y + username_sep_y;
399 		}
400 		mouse_over_line = -1;
401 		return 1;
402 	}
403 
404 	//	Create or toggle open/close the username list window.
405 	//
open(void)406 	void Window::open(void)
407 	{
408 		if (window_id < 0)
409 		{
410 			window_id = create_window (login_select_window_str, -1, 0, 0, 0, 0, 0, ELW_USE_UISCALE|ELW_WIN_DEFAULT);
411 			set_window_handler (window_id, ELW_HANDLER_DISPLAY, (int (*)())&display_pm_handler);
412 			set_window_handler (window_id, ELW_HANDLER_MOUSEOVER, (int (*)())&mouseover_pm_handler);
413 			set_window_handler (window_id, ELW_HANDLER_CLICK, (int (*)())&click_pm_handler);
414 			set_window_handler (window_id, ELW_HANDLER_SHOW, (int (*)())&ui_scale_pm_handler);
415 			set_window_handler (window_id, ELW_HANDLER_UI_SCALE, (int (*)())&ui_scale_pm_handler);
416 			set_window_handler (window_id, ELW_HANDLER_FONT_CHANGE, (int (*)())&change_pm_font_handler);
417 			scroll_id = vscrollbar_add_extended (window_id, 1, NULL, 0, 0, 0, 0, 0, 1.0, 0, 1, 0);
418 			if (window_id >=0 && window_id < windows_list.num_windows)
419 				ui_scale_pm_handler(&windows_list.window[window_id]);
420 		}
421 		else
422 			toggle_window(window_id);
423 	}
424 
425 	//	Destroy the username list window.
426 	//
destroy(void)427 	void Window::destroy(void)
428 	{
429 		destroy_window(window_id);
430 		window_id = -1;
431 	}
432 }
433 
434 static Password_Manaager::Logins * logins = 0;
435 static Password_Manaager::Window * pm_window = 0;
436 
display_pm_handler(window_info * win)437 static int display_pm_handler(window_info *win) { return pm_window->display(logins, win); }
ui_scale_pm_handler(window_info * win)438 static int ui_scale_pm_handler(window_info *win) { return pm_window->ui_scale(logins, win); }
change_pm_font_handler(window_info * win,font_cat cat)439 static int change_pm_font_handler(window_info *win, font_cat cat) { return pm_window->change_font(logins, win, cat); }
click_pm_handler(window_info * win,int mx,int my,Uint32 flags)440 static int click_pm_handler(window_info *win, int mx, int my, Uint32 flags) { return pm_window->click(logins, win, mx, my, flags); }
mouseover_pm_handler(window_info * win,int mx,int my)441 static int mouseover_pm_handler(window_info *win, int mx, int my) { return pm_window->mouseover(logins, win, mx, my); }
click_show_password(widget_list * w,int mx,int my,Uint32 flags)442 static int click_show_password(widget_list *w, int mx, int my, Uint32 flags) { pm_window->toggle_show_password(logins, w); return 1; }
443 
444 //	The externam interface.
445 //
446 extern "C"
447 {
448 	int passmngr_enabled = 0;
449 
passmngr_open_window(void)450 	void passmngr_open_window(void)
451 	{
452 		if (!passmngr_enabled)
453 			return;
454 		if (!pm_window)
455 			pm_window = new Password_Manaager::Window();
456 		passmngr_init();
457 		logins->load();
458 		pm_window->open();
459 	}
460 
passmngr_destroy_window(void)461 	void passmngr_destroy_window(void)
462 	{
463 		if (pm_window)
464 		{
465 			delete pm_window;
466 			pm_window = 0;
467 		}
468 	}
469 
passmngr_save_login(void)470 	void passmngr_save_login(void)
471 	{
472 		if (!passmngr_enabled)
473 			return;
474 		passmngr_init();
475 		logins->add(get_username(), get_password());
476 	}
477 
passmngr_set_login(void)478 	void passmngr_set_login(void)
479 	{
480 		if (!passmngr_enabled)
481 			return;
482 		passmngr_init();
483 		logins->set_details();
484 	}
485 
passmngr_resize(void)486 	void passmngr_resize(void)
487 	{
488 		if (!passmngr_enabled)
489 			return;
490 		if (pm_window)
491 		{
492 			passmngr_init();
493 			pm_window->resize(logins);
494 		}
495 	}
496 
passmngr_pending_pw_change(const char * old_and_new_password)497 	int passmngr_pending_pw_change(const char * old_and_new_password)
498 	{
499 		std::string new_password;
500 		if (!Password_Manaager::Logins::get_validated_new_pasword(old_and_new_password, new_password))
501 			return 0;
502 		if (!passmngr_enabled)
503 			return 1;
504 		passmngr_init();
505 		logins->pending_change(new_password);
506 		return 1;
507 	}
508 
passmngr_confirm_pw_change(void)509 	int passmngr_confirm_pw_change(void)
510 	{
511 		if (!passmngr_enabled)
512 			return 1;
513 		passmngr_init();
514 		return (logins->confirm_change()) ?1: 0;
515 	}
516 
passmngr_init(void)517 	void passmngr_init(void)
518 	{
519 		if (passmngr_enabled && !logins)
520 			logins = new Password_Manaager::Logins();
521 	}
522 
passmngr_destroy(void)523 	void passmngr_destroy(void)
524 	{
525 		passmngr_destroy_window();
526 		if (logins)
527 		{
528 			delete logins;
529 			logins = 0;
530 		}
531 	}
532 }
533