1 /*
2 * ProFTPD - mod_auth_otp database storage
3 * Copyright (c) 2015-2017 TJ Saunders
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 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
18 *
19 * As a special exemption, TJ Saunders and other respective copyright holders
20 * give permission to link this program with OpenSSL, and distribute the
21 * resulting executable, without including the source code for OpenSSL in the
22 * source distribution.
23 */
24
25 #include "mod_auth_otp.h"
26 #include "mod_sql.h"
27 #include "base32.h"
28 #include "db.h"
29
30 #define AUTH_OTP_SQL_VALUE_BUFSZ 32
31
32 /* Max number of attempts for lock requests */
33 #define AUTH_OTP_MAX_LOCK_ATTEMPTS 10
34
35 static const char *trace_channel = "auth_otp";
36
db_get_name(pool * p,const char * name)37 static char *db_get_name(pool *p, const char *name) {
38 cmdtable *cmdtab;
39 cmd_rec *cmd;
40 modret_t *res;
41
42 /* Find the cmdtable for the sql_escapestr command. */
43 cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_escapestr", NULL, NULL, NULL);
44 if (cmdtab == NULL) {
45 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
46 "error: unable to find SQL hook symbol 'sql_escapestr'");
47 return pstrdup(p, name);
48 }
49
50 if (strlen(name) == 0) {
51 return pstrdup(p, "");
52 }
53
54 cmd = pr_cmd_alloc(p, 1, pr_str_strip(p, (char *) name));
55
56 /* Call the handler. */
57 res = pr_module_call(cmdtab->m, cmdtab->handler, cmd);
58
59 /* Check the results. */
60 if (MODRET_ISDECLINED(res) ||
61 MODRET_ISERROR(res)) {
62 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
63 "error executing 'sql_escapestring'");
64 return pstrdup(p, name);
65 }
66
67 return res->data;
68 }
69
auth_otp_db_close(struct auth_otp_db * dbh)70 int auth_otp_db_close(struct auth_otp_db *dbh) {
71 if (dbh->db_lockfd > 0) {
72 (void) close(dbh->db_lockfd);
73 dbh->db_lockfd = -1;
74 }
75
76 destroy_pool(dbh->pool);
77 return 0;
78 }
79
auth_otp_db_open(pool * p,const char * tabinfo)80 struct auth_otp_db *auth_otp_db_open(pool *p, const char *tabinfo) {
81 struct auth_otp_db *dbh = NULL;
82 pool *db_pool = NULL, *tmp_pool = NULL;
83 char *ptr, *ptr2, *named_query, *select_query = NULL, *update_query = NULL;
84 config_rec *c;
85
86 /* The tabinfo should look like:
87 * "/<select-named-query>/<update-named-query>"
88 *
89 * Parse the named queries out of the string, and store them in the db
90 * handle.
91 */
92
93 ptr = strchr(tabinfo, '/');
94 if (ptr == NULL) {
95 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
96 "error: badly formatted table info '%s'", tabinfo);
97 errno = EINVAL;
98 return NULL;
99 }
100
101 ptr2 = strchr(ptr + 1, '/');
102 if (ptr2 == NULL) {
103 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
104 "error: badly formatted table info '%s'", tabinfo);
105 errno = EINVAL;
106 return NULL;
107 }
108
109 db_pool = make_sub_pool(p);
110 pr_pool_tag(db_pool, "Auth OTP Table Pool");
111 dbh = pcalloc(db_pool, sizeof(struct auth_otp_db));
112 dbh->pool = db_pool;
113
114 tmp_pool = make_sub_pool(p);
115
116 *ptr2 = '\0';
117 select_query = pstrdup(dbh->pool, ptr + 1);
118
119 /* Verify that the named query has indeed been defined. This is based on how
120 * mod_sql creates its config_rec names.
121 */
122 named_query = pstrcat(tmp_pool, "SQLNamedQuery_", select_query, NULL);
123 c = find_config(main_server->conf, CONF_PARAM, named_query, FALSE);
124 if (c == NULL) {
125 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
126 "error: unable to resolve SQLNamedQuery name '%s'", select_query);
127 destroy_pool(tmp_pool);
128 errno = EINVAL;
129 return NULL;
130 }
131
132 *ptr = *ptr2 = '/';
133
134 ptr = ptr2;
135 ptr2 = strchr(ptr + 1, '/');
136 if (ptr2 != NULL) {
137 *ptr2 = '\0';
138 }
139
140 update_query = pstrdup(dbh->pool, ptr + 1);
141
142 if (ptr2 != NULL) {
143 *ptr2 = '/';
144 }
145
146 named_query = pstrcat(tmp_pool, "SQLNamedQuery_", update_query, NULL);
147 c = find_config(main_server->conf, CONF_PARAM, named_query, FALSE);
148 if (c == NULL) {
149 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
150 "error: unable to resolve SQLNamedQuery name '%s'", update_query);
151 destroy_pool(tmp_pool);
152 errno = EINVAL;
153 return NULL;
154 }
155
156 destroy_pool(tmp_pool);
157
158 dbh->select_query = select_query;
159 dbh->update_query = update_query;
160
161 /* Prepare the lock structure. */
162 dbh->db_lock.l_whence = SEEK_CUR;
163 dbh->db_lock.l_start = 0;
164 dbh->db_lock.l_len = 0;
165
166 return dbh;
167 }
168
auth_otp_db_get_user_info(pool * p,struct auth_otp_db * dbh,const char * user,const unsigned char ** secret,size_t * secret_len,unsigned long * counter)169 int auth_otp_db_get_user_info(pool *p, struct auth_otp_db *dbh,
170 const char *user, const unsigned char **secret, size_t *secret_len,
171 unsigned long *counter) {
172 int res;
173 pool *tmp_pool = NULL;
174 cmdtable *sql_cmdtab = NULL;
175 cmd_rec *sql_cmd = NULL;
176 modret_t *sql_res = NULL;
177 array_header *sql_data = NULL;
178 const char *select_query = NULL;
179 char *encoded, **values = NULL;
180 size_t encoded_len;
181 unsigned int nvalues = 0;
182
183 if (dbh == NULL ||
184 user == NULL ||
185 secret == NULL ||
186 secret_len == NULL) {
187 errno = EINVAL;
188 return -1;
189 }
190
191 /* Allocate a temporary pool for the duration of this lookup. */
192 tmp_pool = make_sub_pool(p);
193
194 /* Find the cmdtable for the sql_lookup command. */
195 sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_lookup", NULL, NULL,
196 NULL);
197 if (sql_cmdtab == NULL) {
198 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
199 "error: unable to find SQL hook symbol 'sql_lookup'");
200 destroy_pool(tmp_pool);
201 errno = EPERM;
202 return -1;
203 }
204
205 /* Prepare the SELECT query. */
206 select_query = dbh->select_query;
207 sql_cmd = pr_cmd_alloc(tmp_pool, 3, "sql_lookup", select_query,
208 db_get_name(tmp_pool, user));
209
210 /* Call the handler. */
211 sql_res = pr_module_call(sql_cmdtab->m, sql_cmdtab->handler, sql_cmd);
212
213 /* Check the results. */
214 if (sql_res == NULL ||
215 MODRET_ISERROR(sql_res)) {
216 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
217 "error processing SQLNamedQuery '%s'", select_query);
218 destroy_pool(tmp_pool);
219 errno = EPERM;
220 return -1;
221 }
222
223 sql_data = (array_header *) sql_res->data;
224
225 /* The expected number of items in the result set depends on whether we
226 * want/need the HOTP counter. If not, then it's only 1 (for the secret),
227 * otherwise 2 (secret and current counter).
228 */
229 nvalues = (counter ? 2 : 1);
230
231 if (sql_data->nelts < nvalues) {
232 if (sql_data->nelts > 0) {
233 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
234 "error: SQLNamedQuery '%s' returned incorrect number of values (%d)",
235 select_query, sql_data->nelts);
236 }
237
238 destroy_pool(tmp_pool);
239
240 errno = (sql_data->nelts == 0) ? ENOENT : EINVAL;
241 return -1;
242 }
243
244 values = sql_data->elts;
245
246 /* Don't forget to base32-decode the value from the database. */
247 encoded = values[0];
248 encoded_len = strlen(encoded);
249
250 res = auth_otp_base32_decode(p, (const unsigned char *) encoded, encoded_len,
251 secret, secret_len);
252 if (res < 0) {
253 (void) pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
254 "error base32-decoding value from database: %s", strerror(errno));
255 errno = EPERM;
256 return -1;
257 }
258
259 pr_memscrub(values[0], *secret_len);
260
261 if (counter != NULL) {
262 *counter = (unsigned long) atol(values[1]);
263 }
264
265 destroy_pool(tmp_pool);
266 return 0;
267 }
268
auth_otp_db_have_user_info(pool * p,struct auth_otp_db * dbh,const char * user)269 int auth_otp_db_have_user_info(pool *p, struct auth_otp_db *dbh,
270 const char *user) {
271 int res, xerrno = 0;
272 const unsigned char *secret = NULL;
273 size_t secret_len = 0;
274
275 res = auth_otp_db_get_user_info(p, dbh, user, &secret, &secret_len, NULL);
276 xerrno = errno;
277
278 if (res == 0) {
279 pr_memscrub((void *) secret, secret_len);
280 }
281
282 errno = xerrno;
283 return res;
284 }
285
auth_otp_db_update_counter(struct auth_otp_db * dbh,const char * user,unsigned long counter)286 int auth_otp_db_update_counter(struct auth_otp_db *dbh, const char *user,
287 unsigned long counter) {
288 pool *tmp_pool = NULL;
289 cmdtable *sql_cmdtab = NULL;
290 cmd_rec *sql_cmd = NULL;
291 modret_t *sql_res = NULL;
292 const char *update_query = NULL;
293 char *counter_str = NULL;
294 size_t counter_len = 0;
295
296 if (dbh == NULL ||
297 user == NULL) {
298 errno = EINVAL;
299 return -1;
300 }
301
302 /* Allocate a temporary pool for the duration of this change. */
303 tmp_pool = make_sub_pool(dbh->pool);
304
305 /* Find the cmdtable for the sql_change command. */
306 sql_cmdtab = pr_stash_get_symbol2(PR_SYM_HOOK, "sql_change", NULL, NULL,
307 NULL);
308 if (sql_cmdtab == NULL) {
309 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
310 "error: unable to find SQL hook symbol 'sql_change'");
311 destroy_pool(tmp_pool);
312 return -1;
313 }
314
315 update_query = dbh->update_query;
316 counter_len = AUTH_OTP_SQL_VALUE_BUFSZ * sizeof(char);
317 counter_str = pcalloc(tmp_pool, counter_len);
318 pr_snprintf(counter_str, counter_len-1, "%lu", counter);
319
320 sql_cmd = pr_cmd_alloc(tmp_pool, 4, "sql_change", update_query,
321 db_get_name(tmp_pool, user), counter_str);
322
323 /* Call the handler. */
324 sql_res = pr_module_call(sql_cmdtab->m, sql_cmdtab->handler, sql_cmd);
325
326 /* Check the results. */
327 if (sql_res == NULL ||
328 MODRET_ISERROR(sql_res)) {
329 pr_log_writefile(auth_otp_logfd, MOD_AUTH_OTP_VERSION,
330 "error processing SQLNamedQuery '%s'", update_query);
331 destroy_pool(tmp_pool);
332 errno = EPERM;
333 return -1;
334 }
335
336 destroy_pool(tmp_pool);
337 return 0;
338 }
339
340 /* Locking routines */
341
get_lock_type(struct flock * lock)342 static const char *get_lock_type(struct flock *lock) {
343 const char *lock_type;
344
345 switch (lock->l_type) {
346 case F_RDLCK:
347 lock_type = "read-lock";
348 break;
349
350 case F_WRLCK:
351 lock_type = "write-lock";
352 break;
353
354 case F_UNLCK:
355 lock_type = "unlock";
356 break;
357
358 default:
359 lock_type = "[unknown]";
360 }
361
362 return lock_type;
363 }
364
do_lock(int fd,struct flock * lock)365 static int do_lock(int fd, struct flock *lock) {
366 unsigned int nattempts = 1;
367 const char *lock_type;
368
369 lock_type = get_lock_type(lock);
370
371 pr_trace_msg(trace_channel, 9,
372 "attempt #%u to %s AuthOTPTableLock fd %d", nattempts, lock_type, fd);
373
374 while (fcntl(fd, F_SETLK, lock) < 0) {
375 int xerrno = errno;
376
377 if (xerrno == EINTR) {
378 pr_signals_handle();
379 continue;
380 }
381
382 pr_trace_msg(trace_channel, 3,
383 "%s (attempt #%u) of AuthOTPTableLock fd %d failed: %s", lock_type,
384 nattempts, fd, strerror(xerrno));
385 if (xerrno == EACCES) {
386 struct flock locker;
387
388 /* Get the PID of the process blocking this lock. */
389 if (fcntl(fd, F_GETLK, &locker) == 0) {
390 pr_trace_msg(trace_channel, 3, "process ID %lu has blocking %s lock on "
391 "AuthOTPTableLock fd %d", (unsigned long) locker.l_pid,
392 get_lock_type(&locker), fd);
393 }
394 }
395
396 if (xerrno == EAGAIN ||
397 xerrno == EACCES) {
398 /* Treat this as an interrupted call, call pr_signals_handle() (which
399 * will delay for a few msecs because of EINTR), and try again.
400 * After MAX_LOCK_ATTEMPTS attempts, give up altogether.
401 */
402
403 nattempts++;
404 if (nattempts <= AUTH_OTP_MAX_LOCK_ATTEMPTS) {
405 errno = EINTR;
406
407 pr_signals_handle();
408
409 errno = 0;
410 pr_trace_msg(trace_channel, 9,
411 "attempt #%u to %s AuthOTPTableLock fd %d", nattempts, lock_type, fd);
412 continue;
413 }
414
415 pr_trace_msg(trace_channel, 9, "unable to acquire %s on "
416 "AuthOTPTableLock fd %d after %u attempts: %s", lock_type, fd,
417 nattempts, strerror(xerrno));
418 }
419
420 errno = xerrno;
421 return -1;
422 }
423
424 pr_trace_msg(trace_channel, 9,
425 "%s of AuthOTPTableLock fd %d successful after %u %s", lock_type, fd,
426 nattempts, nattempts != 1 ? "attempts" : "attempt");
427 return 0;
428 }
429
auth_otp_db_rlock(struct auth_otp_db * dbh)430 int auth_otp_db_rlock(struct auth_otp_db *dbh) {
431 int res = 0;
432
433 if (dbh->db_lockfd > 0) {
434 dbh->db_lock.l_type = F_RDLCK;
435 res = do_lock(dbh->db_lockfd, &dbh->db_lock);
436 }
437
438 return res;
439 }
440
auth_otp_db_wlock(struct auth_otp_db * dbh)441 int auth_otp_db_wlock(struct auth_otp_db *dbh) {
442 int res = 0;
443
444 if (dbh->db_lockfd > 0) {
445 dbh->db_lock.l_type = F_WRLCK;
446 res = do_lock(dbh->db_lockfd, &dbh->db_lock);
447 }
448
449 return res;
450 }
451
auth_otp_db_unlock(struct auth_otp_db * dbh)452 int auth_otp_db_unlock(struct auth_otp_db *dbh) {
453 int res = 0;
454
455 if (dbh->db_lockfd > 0) {
456 dbh->db_lock.l_type = F_UNLCK;
457 res = do_lock(dbh->db_lockfd, &dbh->db_lock);
458 }
459
460 return res;
461 }
462