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/password.h>
27 :
28 : #include <safepasswords/exception.h>
29 :
30 :
31 : // snapdev
32 : //
33 : #include <snapdev/not_used.h>
34 :
35 :
36 : // libutf8
37 : //
38 : #include <libutf8/libutf8.h>
39 :
40 :
41 : // C++
42 : //
43 : #include <set>
44 :
45 :
46 : // C
47 : //
48 : #include <malloc.h>
49 :
50 :
51 : // last include
52 : //
53 : #include <snapdev/poison.h>
54 :
55 :
56 :
57 4 : CATCH_TEST_CASE("password", "[password]")
58 : {
59 4 : CATCH_START_SECTION("password: verify constructor (empty)")
60 : {
61 1 : safepasswords::password p;
62 1 : CATCH_REQUIRE(p.get_digest() == "sha512"); // digest has a default
63 1 : CATCH_REQUIRE(p.get_plain().empty());
64 1 : CATCH_REQUIRE(p.get_salt().empty());
65 :
66 : // the get_encrypted() actually generates a password if none is
67 : // defined, so it is not going to be empty ever and then the
68 : // plain & salt strings are also not empty after this call
69 : //
70 1 : CATCH_REQUIRE_FALSE(p.get_encrypted().empty());
71 :
72 1 : CATCH_REQUIRE_FALSE(p.get_plain().empty());
73 1 : CATCH_REQUIRE_FALSE(p.get_salt().empty());
74 :
75 : // try setting it back, this clears the plain password
76 : //
77 1 : safepasswords::string encrypted(p.get_encrypted());
78 1 : safepasswords::string salt(p.get_salt());
79 1 : p.set_encrypted(encrypted, salt);
80 :
81 1 : CATCH_REQUIRE(p.get_encrypted() == encrypted);
82 1 : CATCH_REQUIRE(p.get_plain().empty());
83 1 : CATCH_REQUIRE(p.get_salt() == salt);
84 :
85 : // test the clear() function
86 : //
87 1 : p.clear();
88 1 : CATCH_REQUIRE(p.get_plain().empty());
89 1 : CATCH_REQUIRE(p.get_salt().empty());
90 1 : }
91 4 : CATCH_END_SECTION()
92 :
93 4 : CATCH_START_SECTION("password: compare passwords with varying digests")
94 : {
95 : // try the following command to get a list of existing digests:
96 : //
97 : // generate-password --list-digests
98 : //
99 1 : char const * const digests[] = {
100 : "sha256",
101 : "sha512",
102 : "md5",
103 : "2.16.840.1.101.3.4.2.11", // SHAKE-128
104 : "sha1",
105 : "ssl3-md5",
106 : "sha384",
107 : "sha224",
108 : "sm3",
109 : "blake2s256",
110 : };
111 11 : for(std::size_t idx(0); idx < std::size(digests); ++idx)
112 : {
113 10 : std::string const p1(SNAP_CATCH2_NAMESPACE::random_string(1, 100));
114 10 : std::string const p2(SNAP_CATCH2_NAMESPACE::random_string(1, 100));
115 :
116 10 : safepasswords::password password1;
117 10 : password1.set_digest(digests[idx]);
118 10 : password1.set_plain(safepasswords::string(p1.c_str(), p1.length()));
119 10 : safepasswords::password password2;
120 10 : password2.set_digest(digests[(idx + rand() % 20) % std::size(digests)]);
121 10 : password2.set_plain(safepasswords::string(p2.c_str(), p2.length()));
122 :
123 10 : auto r(password1 <=> password2);
124 10 : auto q(password1.get_encrypted().to_std_string() <=> password2.get_encrypted().to_std_string());
125 10 : CATCH_REQUIRE(r == q);
126 10 : }
127 : }
128 4 : CATCH_END_SECTION()
129 :
130 4 : CATCH_START_SECTION("password: generate short passwords")
131 : {
132 11 : for(int idx(0); idx < 10; ++idx)
133 : {
134 10 : int const min_len(rand() % 16 + 16);
135 10 : int const max_len(min_len + rand() % 10);
136 :
137 10 : safepasswords::password p;
138 10 : p.generate(min_len, max_len);
139 :
140 10 : CATCH_REQUIRE(p.get_plain().length() >= static_cast<std::size_t>(min_len));
141 10 : CATCH_REQUIRE(p.get_plain().length() <= static_cast<std::size_t>(max_len));
142 :
143 : // try again with equal min/max
144 : //
145 10 : p.generate(min_len, min_len);
146 10 : CATCH_REQUIRE(p.get_plain().length() == static_cast<std::size_t>(min_len));
147 10 : }
148 : }
149 4 : CATCH_END_SECTION()
150 :
151 4 : CATCH_START_SECTION("password: generate passwords with negative max. length")
152 : {
153 11 : for(int idx(0); idx < 10; ++idx)
154 : {
155 10 : int const min_len(rand() % 16 + 16);
156 10 : int const max_len(-min_len - rand() % 10);
157 :
158 10 : safepasswords::password p;
159 10 : p.generate(min_len, max_len);
160 :
161 10 : CATCH_REQUIRE(p.get_plain().length() >= static_cast<std::size_t>(min_len));
162 10 : CATCH_REQUIRE(p.get_plain().length() <= safepasswords::PASSWORD_DEFAULT_MAX_LENGTH);
163 10 : }
164 : }
165 4 : CATCH_END_SECTION()
166 4 : }
167 :
168 :
169 4 : CATCH_TEST_CASE("password_invalid", "[password][error]")
170 : {
171 4 : CATCH_START_SECTION("password_invalid: unknown digest")
172 : {
173 1 : safepasswords::password bad_digest;
174 :
175 5 : CATCH_REQUIRE_THROWS_MATCHES(
176 : bad_digest.set_digest("bad-digest")
177 : , safepasswords::digest_not_available
178 : , Catch::Matchers::ExceptionMessage(
179 : "safepasswords_exception: the specified digest (bad-digest) was not found."));
180 :
181 1 : CATCH_REQUIRE(bad_digest.get_digest() == "sha512");
182 :
183 : // the get_encrypted() still works, it just uses the default digest
184 : //
185 1 : bad_digest.get_encrypted();
186 1 : }
187 4 : CATCH_END_SECTION()
188 :
189 4 : CATCH_START_SECTION("password_invalid: invalid salt length")
190 : {
191 1 : safepasswords::password bad_salt;
192 1 : safepasswords::string p("test", 4);
193 1 : safepasswords::string s("bad", 3);
194 :
195 1 : CATCH_REQUIRE_THROWS_MATCHES(
196 : bad_salt.set_plain(p, s)
197 : , safepasswords::invalid_parameter
198 : , Catch::Matchers::ExceptionMessage(
199 : "safepasswords_exception: if defined, the salt must be exactly 32 bytes."));
200 1 : }
201 4 : CATCH_END_SECTION()
202 :
203 4 : CATCH_START_SECTION("password_invalid: generate passwords where min > max")
204 : {
205 11 : for(int idx(0); idx < 10; ++idx)
206 : {
207 10 : int const min_len(rand() % 16 + 16);
208 10 : int const max_len(min_len + rand() % 10 + 1);
209 :
210 10 : safepasswords::password p;
211 :
212 10 : CATCH_REQUIRE_THROWS_MATCHES(
213 : p.generate(max_len, min_len)
214 : , safepasswords::invalid_parameter
215 : , Catch::Matchers::ExceptionMessage(
216 : "safepasswords_exception: adjusted minimum and"
217 : " maximum lengths are improperly sorted; minimum "
218 : + std::to_string(max_len) // it is swapped above, so swapped here too
219 : + " is larger than maximum "
220 : + std::to_string(min_len)
221 : + '.'));
222 10 : }
223 : }
224 4 : CATCH_END_SECTION()
225 :
226 4 : CATCH_START_SECTION("password_invalid: generate passwords where max < 8")
227 : {
228 9 : for(int max_len(0); max_len < safepasswords::PASSWORD_MIN_LENGTH; ++max_len)
229 : {
230 8 : safepasswords::password p;
231 :
232 : // minimum gets adjusted to safepasswords::PASSWORD_MIN_LENGTH
233 : // so any maximum that is smaller than that value will generate
234 : // an error
235 : //
236 8 : CATCH_REQUIRE_THROWS_MATCHES(
237 : p.generate(0, max_len)
238 : , safepasswords::invalid_parameter
239 : , Catch::Matchers::ExceptionMessage(
240 : "safepasswords_exception: adjusted minimum and "
241 : "maximum lengths are improperly sorted; minimum "
242 : + std::to_string(safepasswords::PASSWORD_MIN_LENGTH) // the adjusted minimum
243 : + " is larger than maximum "
244 : + std::to_string(max_len)
245 : + '.'));
246 8 : }
247 : }
248 4 : CATCH_END_SECTION()
249 4 : }
250 :
251 :
252 :
253 : // vim: ts=4 sw=4 et
|