Line data Source code
1 : // Copyright (c) 2019-2023 Made to Order Software Corp. All Rights Reserved
2 : //
3 : // https://snapwebsites.org/project/safepasswords
4 : // contact@m2osw.com
5 : //
6 : // This program is free software: you can redistribute it and/or modify
7 : // it under the terms of the GNU General Public License as published by
8 : // the Free Software Foundation, either version 3 of the License, or
9 : // (at your option) any later version.
10 : //
11 : // This program is distributed in the hope that it will be useful,
12 : // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 : // GNU General Public License for more details.
15 : //
16 : // You should have received a copy of the GNU General Public License
17 : // along with this program. If not, see <https://www.gnu.org/licenses/>.
18 :
19 : // self
20 : //
21 : #include "catch_main.h"
22 :
23 :
24 : // safepasswords
25 : //
26 : #include <safepasswords/file.h>
27 :
28 : #include <safepasswords/exception.h>
29 :
30 :
31 : // snapdev
32 : //
33 : #include <snapdev/hexadecimal_string.h>
34 : #include <snapdev/not_used.h>
35 :
36 :
37 : // libutf8
38 : //
39 : #include <libutf8/libutf8.h>
40 :
41 :
42 : // C++
43 : //
44 : #include <set>
45 :
46 :
47 : // C
48 : //
49 : #include <malloc.h>
50 :
51 :
52 : // last include
53 : //
54 : #include <snapdev/poison.h>
55 :
56 :
57 :
58 : namespace
59 : {
60 :
61 :
62 11 : std::string get_tmp_dir(char const * filename)
63 : {
64 22 : return SNAP_CATCH2_NAMESPACE::g_tmp_dir() + '/' + filename;
65 : }
66 :
67 :
68 : }
69 : // no name namespace
70 :
71 :
72 :
73 1 : CATCH_TEST_CASE("file_load", "[file][load][save]")
74 : {
75 1 : CATCH_START_SECTION("file_load: create file with a few entries")
76 : {
77 1 : std::string const filename(get_tmp_dir("entries.pwd"));
78 : {
79 1 : std::ofstream out;
80 1 : out.open(filename);
81 1 : CATCH_REQUIRE(out.is_open());
82 :
83 : // valid entries
84 : //
85 : out << "name:sha512:"
86 2 : << snapdev::bin_to_hex("salt--0123456789")
87 : << ':'
88 2 : << snapdev::bin_to_hex("encrypted-password")
89 3 : << '\n';
90 : out << "user:sha3-224:"
91 2 : << snapdev::bin_to_hex("the-salt-is-1234")
92 : << ':'
93 2 : << snapdev::bin_to_hex("another-secret-password")
94 3 : << '\n';
95 : out << "walker:sha3-256:"
96 2 : << snapdev::bin_to_hex("salt-is-16bytes!")
97 : << ':'
98 2 : << snapdev::bin_to_hex("you'll never find this_passworD1")
99 3 : << '\n';
100 : out << "alexa:md5:"
101 2 : << snapdev::bin_to_hex("more salt here..")
102 : << ':'
103 2 : << snapdev::bin_to_hex("wonder what the password is?")
104 3 : << '\n';
105 : out << "lexa:sha1:"
106 2 : << snapdev::bin_to_hex("salt is randomiz")
107 : << ':'
108 2 : << snapdev::bin_to_hex("and SHA1 is really bad, don't use it")
109 3 : << '\n';
110 : out << "lex:sha3-256:"
111 2 : << snapdev::bin_to_hex("short name here!")
112 : << ':'
113 2 : << snapdev::bin_to_hex("old version would not find \"lex\" when we have \"alexa\" and \"lexa\" before...")
114 3 : << '\n';
115 :
116 : // invalid entries
117 : //
118 : out << "no-password:sha3-256:"
119 2 : << snapdev::bin_to_hex("salt-is-16bytes!")
120 2 : << '\n';
121 : out << "missing-salt:sha3-256"
122 1 : << '\n';
123 : out << "without-digest" // this means we do not find that user
124 1 : << '\n';
125 :
126 : // MUST BE LAST -- ADD MORE BEFORE THIS ENTRY
127 : // invalid entry -- the "\n" is missing
128 : //
129 1 : out << "missing:newline:character:here";
130 1 : }
131 1 : safepasswords::file f(filename);
132 1 : safepasswords::password p;
133 :
134 : // first try one which does not exist
135 : //
136 1 : CATCH_REQUIRE_FALSE(f.find("anyone", p));
137 :
138 1 : CATCH_REQUIRE(f.find("name", p));
139 1 : CATCH_REQUIRE(p.get_plain().empty());
140 1 : CATCH_REQUIRE(p.get_digest() == "sha512");
141 1 : CATCH_REQUIRE(p.get_salt() == "salt--0123456789");
142 1 : CATCH_REQUIRE(p.get_encrypted() == "encrypted-password");
143 :
144 1 : CATCH_REQUIRE(f.find("user", p));
145 1 : CATCH_REQUIRE(p.get_plain().empty());
146 1 : CATCH_REQUIRE(p.get_digest() == "sha3-224");
147 1 : CATCH_REQUIRE(p.get_salt() == "the-salt-is-1234");
148 1 : CATCH_REQUIRE(p.get_encrypted() == "another-secret-password");
149 :
150 1 : CATCH_REQUIRE(f.find("walker", p));
151 1 : CATCH_REQUIRE(p.get_plain().empty());
152 1 : CATCH_REQUIRE(p.get_digest() == "sha3-256");
153 1 : CATCH_REQUIRE(p.get_salt() == "salt-is-16bytes!");
154 1 : CATCH_REQUIRE(p.get_encrypted() == "you'll never find this_passworD1");
155 :
156 1 : CATCH_REQUIRE(f.find("alexa", p));
157 1 : CATCH_REQUIRE(p.get_plain().empty());
158 1 : CATCH_REQUIRE(p.get_digest() == "md5");
159 1 : CATCH_REQUIRE(p.get_salt() == "more salt here..");
160 1 : CATCH_REQUIRE(p.get_encrypted() == "wonder what the password is?");
161 :
162 1 : CATCH_REQUIRE(f.find("lexa", p));
163 1 : CATCH_REQUIRE(p.get_plain().empty());
164 1 : CATCH_REQUIRE(p.get_digest() == "sha1");
165 1 : CATCH_REQUIRE(p.get_salt() == "salt is randomiz");
166 1 : CATCH_REQUIRE(p.get_encrypted() == "and SHA1 is really bad, don't use it");
167 :
168 1 : CATCH_REQUIRE(f.find("lex", p));
169 1 : CATCH_REQUIRE(p.get_plain().empty());
170 1 : CATCH_REQUIRE(p.get_digest() == "sha3-256");
171 1 : CATCH_REQUIRE(p.get_salt() == "short name here!");
172 1 : CATCH_REQUIRE(p.get_encrypted() == "old version would not find \"lex\" when we have \"alexa\" and \"lexa\" before...");
173 :
174 1 : CATCH_REQUIRE_FALSE(f.find("no-password", p));
175 1 : CATCH_REQUIRE_FALSE(f.find("missing-salt", p));
176 1 : CATCH_REQUIRE_FALSE(f.find("without-digest", p));
177 :
178 1 : CATCH_REQUIRE_FALSE(f.find("missing", p)); // newline not in string
179 :
180 : // these reset the password
181 : //
182 1 : CATCH_REQUIRE(p.get_plain().empty());
183 1 : CATCH_REQUIRE(p.get_digest() == "sha3-256");
184 1 : CATCH_REQUIRE(p.get_salt().empty());
185 :
186 1 : CATCH_REQUIRE_FALSE(p.get_encrypted().empty()); // this regenerates a new password
187 :
188 1 : f.save("some_new_user", p);
189 :
190 : // also try replacing an existing user
191 : //
192 1 : safepasswords::password q;
193 1 : CATCH_REQUIRE_FALSE(q.get_encrypted().empty());
194 1 : f.save("user", q);
195 1 : }
196 1 : CATCH_END_SECTION()
197 1 : }
198 :
199 :
200 2 : CATCH_TEST_CASE("file_remove", "[file][save][load][remove]")
201 : {
202 2 : CATCH_START_SECTION("file_remove: create file, close, re-load, remove, re-re-load, re-remove")
203 : {
204 1 : char const * user_names[] = {
205 : "charlie",
206 : "hubert",
207 : "julius",
208 : "pompom",
209 : "henri",
210 : "louis",
211 : "jesus",
212 : };
213 1 : std::string const filename(get_tmp_dir("removal.pwd"));
214 :
215 1 : std::vector<safepasswords::password> v;
216 : {
217 1 : safepasswords::file f(filename);
218 :
219 8 : for(std::size_t idx(0); idx < std::size(user_names); ++idx)
220 : {
221 7 : safepasswords::password p;
222 7 : p.generate(); // this regenerates a new password
223 7 : CATCH_REQUIRE(f.save(user_names[idx], p));
224 7 : v.push_back(p);
225 7 : }
226 1 : }
227 :
228 : {
229 1 : safepasswords::file f(filename);
230 :
231 8 : for(std::size_t idx(0); idx < std::size(user_names); ++idx)
232 : {
233 7 : safepasswords::password p;
234 7 : CATCH_REQUIRE(f.find(user_names[idx], p));
235 :
236 : // make sure it did not change between the save + load
237 : //
238 7 : CATCH_REQUIRE(v[idx].get_digest() == p.get_digest());
239 7 : CATCH_REQUIRE(v[idx].get_salt() == p.get_salt());
240 7 : CATCH_REQUIRE(v[idx].get_encrypted() == p.get_encrypted());
241 :
242 : // remove the odd ones
243 : //
244 7 : if((idx & 1) != 0)
245 : {
246 3 : CATCH_REQUIRE(f.remove(user_names[idx]));
247 : }
248 7 : }
249 1 : }
250 :
251 : {
252 1 : safepasswords::file f(filename);
253 :
254 8 : for(std::size_t idx(0); idx < std::size(user_names); ++idx)
255 : {
256 7 : safepasswords::password p;
257 7 : if((idx & 1) != 0)
258 : {
259 : // this one was removed, so we won't find it
260 : //
261 3 : CATCH_REQUIRE_FALSE(f.find(user_names[idx], p));
262 :
263 : // we can re-remove, nothing happens
264 : //
265 3 : CATCH_REQUIRE(f.remove(user_names[idx]));
266 : }
267 : else
268 : {
269 4 : CATCH_REQUIRE(f.find(user_names[idx], p));
270 :
271 : // make sure it's still there
272 : //
273 4 : CATCH_REQUIRE(v[idx].get_digest() == p.get_digest());
274 4 : CATCH_REQUIRE(v[idx].get_salt() == p.get_salt());
275 4 : CATCH_REQUIRE(v[idx].get_encrypted() == p.get_encrypted());
276 : }
277 7 : }
278 1 : }
279 :
280 1 : }
281 2 : CATCH_END_SECTION()
282 :
283 2 : CATCH_START_SECTION("file_remove: missing '\\n'")
284 : {
285 1 : std::string const filename(get_tmp_dir("missing-nl.pwd"));
286 : {
287 1 : std::ofstream out;
288 1 : out.open(filename);
289 1 : CATCH_REQUIRE(out.is_open());
290 :
291 : // valid entry
292 : //
293 : out << "correct:md5:"
294 2 : << snapdev::bin_to_hex("salt--0123456789")
295 : << ':'
296 2 : << snapdev::bin_to_hex("entry for once")
297 3 : << '\n';
298 :
299 : // invalid entry
300 : //
301 : out << "missing-nl:sha512:" // missing '\n' at the end
302 2 : << snapdev::bin_to_hex("even more salt!!")
303 : << ':'
304 2 : << snapdev::bin_to_hex("entry for once");
305 1 : }
306 :
307 : {
308 1 : safepasswords::file f(filename);
309 1 : safepasswords::password p;
310 :
311 1 : CATCH_REQUIRE(f.next(p) == "correct");
312 1 : CATCH_REQUIRE(f.next(p).empty()); // we already cannot load "missing-nl" in this case
313 :
314 1 : CATCH_REQUIRE(f.remove("missing-nl"));
315 1 : }
316 :
317 : {
318 1 : safepasswords::file f(filename);
319 1 : safepasswords::password p;
320 :
321 1 : CATCH_REQUIRE(f.next(p) == "correct");
322 1 : CATCH_REQUIRE(f.next(p).empty()); // so this tests nothing since it was already not loadable before the remove...
323 :
324 : // but I could verify by hand and it works...
325 : //
326 : // TODO:
327 : // what we would have to do is create a memory version of what
328 : // is expected in the file and reload with file_contents to
329 : // make sure it is equal to what's expected
330 1 : }
331 1 : }
332 2 : CATCH_END_SECTION()
333 2 : }
334 :
335 :
336 4 : CATCH_TEST_CASE("file_iterate", "[file][iterate][load]")
337 : {
338 4 : CATCH_START_SECTION("file_iterate: create file, close, load, go through with next()")
339 : {
340 1 : char const * user_names[] = {
341 : "charlotte",
342 : "henriette",
343 : "julia",
344 : "ponpon",
345 : "theo",
346 : "louisa",
347 : "angel",
348 : };
349 1 : std::string const filename(get_tmp_dir("iterate.pwd"));
350 :
351 1 : std::vector<safepasswords::password> v;
352 : {
353 1 : safepasswords::file f(filename);
354 :
355 8 : for(std::size_t idx(0); idx < std::size(user_names); ++idx)
356 : {
357 7 : safepasswords::password p;
358 7 : p.generate(); // this regenerates a new password
359 7 : CATCH_REQUIRE(f.save(user_names[idx], p));
360 7 : v.push_back(p);
361 7 : }
362 1 : }
363 :
364 : {
365 1 : safepasswords::file f(filename);
366 :
367 1 : for(std::size_t idx(0);; ++idx)
368 : {
369 8 : safepasswords::password p;
370 8 : std::string const username(f.next(p));
371 8 : if(username.empty())
372 : {
373 1 : std::string const still_empty(f.next(p));
374 1 : CATCH_REQUIRE(still_empty.empty());
375 1 : break;
376 1 : }
377 :
378 : // make sure it did not change between the save + load
379 : //
380 7 : CATCH_REQUIRE(idx < std::size(user_names));
381 7 : CATCH_REQUIRE(username == user_names[idx]);
382 7 : CATCH_REQUIRE(v[idx].get_digest() == p.get_digest());
383 7 : CATCH_REQUIRE(v[idx].get_salt() == p.get_salt());
384 7 : CATCH_REQUIRE(v[idx].get_encrypted() == p.get_encrypted());
385 16 : }
386 :
387 : // do it a second time after a rewind
388 : //
389 1 : f.rewind();
390 :
391 1 : for(std::size_t idx(0);; ++idx)
392 : {
393 8 : safepasswords::password p;
394 8 : std::string const username(f.next(p));
395 8 : if(username.empty())
396 : {
397 1 : std::string const still_empty(f.next(p));
398 1 : CATCH_REQUIRE(still_empty.empty());
399 1 : break;
400 1 : }
401 :
402 : // make sure it did not change between the save + load
403 : //
404 7 : CATCH_REQUIRE(idx < std::size(user_names));
405 7 : CATCH_REQUIRE(username == user_names[idx]);
406 7 : CATCH_REQUIRE(v[idx].get_digest() == p.get_digest());
407 7 : CATCH_REQUIRE(v[idx].get_salt() == p.get_salt());
408 7 : CATCH_REQUIRE(v[idx].get_encrypted() == p.get_encrypted());
409 16 : }
410 1 : }
411 1 : }
412 4 : CATCH_END_SECTION()
413 :
414 4 : CATCH_START_SECTION("file_iterate: name not followed by ':'")
415 : {
416 1 : std::string const filename(get_tmp_dir("entries.pwd"));
417 : {
418 1 : std::ofstream out;
419 1 : out.open(filename);
420 1 : CATCH_REQUIRE(out.is_open());
421 :
422 : // valid entry
423 : //
424 : out << "valid:sha512:"
425 2 : << snapdev::bin_to_hex("salt--0123456789")
426 : << ':'
427 2 : << snapdev::bin_to_hex("entry for once")
428 3 : << '\n';
429 :
430 : // invalid entry
431 : //
432 : out << "without-digest" // no ':' after user name
433 1 : << '\n';
434 1 : }
435 1 : safepasswords::file f(filename);
436 1 : safepasswords::password p;
437 :
438 : // first is valid
439 : //
440 1 : CATCH_REQUIRE(f.next(p) == "valid");
441 1 : CATCH_REQUIRE(p.get_plain().empty());
442 1 : CATCH_REQUIRE(p.get_digest() == "sha512");
443 1 : CATCH_REQUIRE(p.get_salt() == "salt--0123456789");
444 1 : CATCH_REQUIRE(p.get_encrypted() == "entry for once");
445 :
446 1 : CATCH_REQUIRE(f.next(p).empty());
447 1 : CATCH_REQUIRE(p.get_plain().empty());
448 1 : CATCH_REQUIRE(p.get_digest() == "sha512");
449 1 : CATCH_REQUIRE(p.get_salt().empty());
450 1 : }
451 4 : CATCH_END_SECTION()
452 :
453 4 : CATCH_START_SECTION("file_iterate: empty name not valid")
454 : {
455 1 : std::string const filename(get_tmp_dir("entries.pwd"));
456 : {
457 1 : std::ofstream out;
458 1 : out.open(filename);
459 1 : CATCH_REQUIRE(out.is_open());
460 :
461 : // valid entry
462 : //
463 : out << "correct:md5:"
464 2 : << snapdev::bin_to_hex("salt--0123456789")
465 : << ':'
466 2 : << snapdev::bin_to_hex("entry for once")
467 3 : << '\n';
468 :
469 : // invalid entry
470 : //
471 : out << ":sha512:" // no name before ':'
472 2 : << snapdev::bin_to_hex("even more salt!!")
473 : << ':'
474 2 : << snapdev::bin_to_hex("name is missing")
475 3 : << '\n';
476 1 : }
477 1 : safepasswords::file f(filename);
478 1 : safepasswords::password p;
479 :
480 : // first is valid
481 : //
482 1 : CATCH_REQUIRE(f.next(p) == "correct");
483 1 : CATCH_REQUIRE(p.get_plain().empty());
484 1 : CATCH_REQUIRE(p.get_digest() == "md5");
485 1 : CATCH_REQUIRE(p.get_salt() == "salt--0123456789");
486 1 : CATCH_REQUIRE(p.get_encrypted() == "entry for once");
487 :
488 1 : CATCH_REQUIRE(f.next(p).empty());
489 1 : CATCH_REQUIRE(p.get_plain().empty());
490 1 : CATCH_REQUIRE(p.get_digest() == "md5");
491 1 : CATCH_REQUIRE(p.get_salt().empty());
492 1 : }
493 4 : CATCH_END_SECTION()
494 :
495 4 : CATCH_START_SECTION("file_iterate: missing encrypted value")
496 : {
497 1 : std::string const filename(get_tmp_dir("entries.pwd"));
498 : {
499 1 : std::ofstream out;
500 1 : out.open(filename);
501 1 : CATCH_REQUIRE(out.is_open());
502 :
503 : // valid entry
504 : //
505 : out << "correct:md5:"
506 2 : << snapdev::bin_to_hex("salt--0123456789")
507 : << ':'
508 2 : << snapdev::bin_to_hex("entry for once")
509 3 : << '\n';
510 :
511 : // invalid entry
512 : //
513 : out << "missing-stuff:sha512:" // missing encrypted value
514 2 : << snapdev::bin_to_hex("even more salt!!")
515 2 : << '\n';
516 1 : }
517 1 : safepasswords::file f(filename);
518 1 : safepasswords::password p;
519 :
520 : // first is valid
521 : //
522 1 : CATCH_REQUIRE(f.next(p) == "correct");
523 1 : CATCH_REQUIRE(p.get_plain().empty());
524 1 : CATCH_REQUIRE(p.get_digest() == "md5");
525 1 : CATCH_REQUIRE(p.get_salt() == "salt--0123456789");
526 1 : CATCH_REQUIRE(p.get_encrypted() == "entry for once");
527 :
528 1 : CATCH_REQUIRE(f.next(p).empty());
529 1 : CATCH_REQUIRE(p.get_plain().empty());
530 1 : CATCH_REQUIRE(p.get_digest() == "md5");
531 1 : CATCH_REQUIRE(p.get_salt().empty());
532 1 : }
533 4 : CATCH_END_SECTION()
534 4 : }
535 :
536 :
537 2 : CATCH_TEST_CASE("file_missing", "[file][load][save]")
538 : {
539 2 : CATCH_START_SECTION("file_missing: attempt to load missing file, find fails")
540 : {
541 2 : safepasswords::file f(get_tmp_dir("missing.pwd"));
542 1 : safepasswords::password p;
543 1 : CATCH_REQUIRE_FALSE(f.find("anyone", p));
544 1 : }
545 2 : CATCH_END_SECTION()
546 :
547 2 : CATCH_START_SECTION("file_missing: attempt to load missing file, next fails")
548 : {
549 2 : safepasswords::file f(get_tmp_dir("missing.pwd"));
550 1 : safepasswords::password p;
551 1 : CATCH_REQUIRE(f.next(p).empty());
552 1 : }
553 2 : CATCH_END_SECTION()
554 2 : }
555 :
556 :
557 2 : CATCH_TEST_CASE("file_with_errors", "[file][error]")
558 : {
559 2 : CATCH_START_SECTION("file_with_errors: verify invalid find() call")
560 : {
561 2 : safepasswords::file f(get_tmp_dir("ignore.pwd"));
562 1 : safepasswords::password p;
563 3 : CATCH_REQUIRE_THROWS_MATCHES(
564 : f.find(std::string(), p)
565 : , safepasswords::invalid_parameter
566 : , Catch::Matchers::ExceptionMessage(
567 : "safepasswords_exception: the file::find()/file::save()/file::remove() functions"
568 : " cannot be called with an empty string in 'name'."));
569 1 : }
570 2 : CATCH_END_SECTION()
571 :
572 2 : CATCH_START_SECTION("file_with_errors: verify invalid save() call")
573 : {
574 2 : safepasswords::file f(get_tmp_dir("ignore.pwd"));
575 1 : safepasswords::password p;
576 3 : CATCH_REQUIRE_THROWS_MATCHES(
577 : f.save(std::string(), p)
578 : , safepasswords::invalid_parameter
579 : , Catch::Matchers::ExceptionMessage(
580 : "safepasswords_exception: the file::find()/file::save()/file::remove() functions"
581 : " cannot be called with an empty string in 'name'."));
582 1 : }
583 2 : CATCH_END_SECTION()
584 2 : }
585 :
586 :
587 :
588 : // vim: ts=4 sw=4 et
|