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