1 /* $NetBSD: smtp_sasl_auth_cache.c,v 1.3 2020/03/18 19:05:20 christos Exp $ */
2
3 /*++
4 /* NAME
5 /* smtp_sasl_auth_cache 3
6 /* SUMMARY
7 /* Postfix SASL authentication reply cache
8 /* SYNOPSIS
9 /* #include "smtp.h"
10 /* #include "smtp_sasl_auth_cache.h"
11 /*
12 /* SMTP_SASL_AUTH_CACHE *smtp_sasl_auth_cache_init(map, ttl)
13 /* const char *map
14 /* int ttl;
15 /*
16 /* void smtp_sasl_auth_cache_store(auth_cache, session, resp)
17 /* SMTP_SASL_AUTH_CACHE *auth_cache;
18 /* const SMTP_SESSION *session;
19 /* const SMTP_RESP *resp;
20 /*
21 /* int smtp_sasl_auth_cache_find(auth_cache, session)
22 /* SMTP_SASL_AUTH_CACHE *auth_cache;
23 /* const SMTP_SESSION *session;
24 /*
25 /* char *smtp_sasl_auth_cache_dsn(auth_cache)
26 /* SMTP_SASL_AUTH_CACHE *auth_cache;
27 /*
28 /* char *smtp_sasl_auth_cache_text(auth_cache)
29 /* SMTP_SASL_AUTH_CACHE *auth_cache;
30 /* DESCRIPTION
31 /* This module maintains a cache of SASL authentication server replies.
32 /* This can be used to avoid repeated login failure errors.
33 /*
34 /* smtp_sasl_auth_cache_init() opens or creates the named cache.
35 /*
36 /* smtp_sasl_auth_cache_store() stores information about a
37 /* SASL login attempt together with the server status and
38 /* complete response.
39 /*
40 /* smtp_sasl_auth_cache_find() returns non-zero when a cache
41 /* entry exists for the given host, username and password.
42 /*
43 /* smtp_sasl_auth_cache_dsn() and smtp_sasl_auth_cache_text()
44 /* return the status and complete server response as found
45 /* with smtp_sasl_auth_cache_find().
46 /*
47 /* Arguments:
48 /* .IP map
49 /* Lookup table name. The name must be singular and must start
50 /* with "proxy:".
51 /* .IP ttl
52 /* The time after which a cache entry is considered expired.
53 /* .IP session
54 /* Session context.
55 /* .IP resp
56 /* Remote SMTP server response, to be stored into the cache.
57 /* DIAGNOSTICS
58 /* All errors are fatal.
59 /* LICENSE
60 /* .ad
61 /* .fi
62 /* The Secure Mailer license must be distributed with this software.
63 /* AUTHOR(S)
64 /* Original author:
65 /* Keean Schupke
66 /* Fry-IT Ltd.
67 /*
68 /* Updated by:
69 /* Wietse Venema
70 /* IBM T.J. Watson Research
71 /* P.O. Box 704
72 /* Yorktown Heights, NY 10598, USA
73 /*--*/
74
75 /*
76 * System library.
77 */
78 #include <sys_defs.h>
79
80 /*
81 * Utility library
82 */
83 #include <msg.h>
84 #include <mymalloc.h>
85 #include <stringops.h>
86 #include <base64_code.h>
87 #include <dict.h>
88
89 /*
90 * Global library
91 */
92 #include <dsn_util.h>
93 #include <dict_proxy.h>
94
95 /*
96 * Application-specific
97 */
98 #include "smtp.h"
99 #include "smtp_sasl_auth_cache.h"
100
101 /*
102 * XXX This feature stores passwords, so we must mask them with a strong
103 * cryptographic hash. This requires OpenSSL support.
104 *
105 * XXX It would be even better if the stored hash were salted.
106 */
107 #ifdef HAVE_SASL_AUTH_CACHE
108
109 /* smtp_sasl_auth_cache_init - per-process initialization (pre jail) */
110
smtp_sasl_auth_cache_init(const char * map,int ttl)111 SMTP_SASL_AUTH_CACHE *smtp_sasl_auth_cache_init(const char *map, int ttl)
112 {
113 const char *myname = "smtp_sasl_auth_cache_init";
114 SMTP_SASL_AUTH_CACHE *auth_cache;
115
116 /*
117 * Sanity checks.
118 */
119 #define HAS_MULTIPLE_VALUES(s) ((s)[strcspn((s), CHARS_COMMA_SP)] != 0)
120
121 if (*map == 0)
122 msg_panic("%s: empty SASL authentication cache name", myname);
123 if (ttl < 0)
124 msg_panic("%s: bad SASL authentication cache ttl: %d", myname, ttl);
125 if (HAS_MULTIPLE_VALUES(map))
126 msg_fatal("SASL authentication cache name \"%s\" "
127 "contains multiple values", map);
128
129 /*
130 * XXX To avoid multiple writers the map needs to be maintained by the
131 * proxywrite service. We would like to have a DICT_FLAG_REQ_PROXY flag
132 * so that the library can enforce this, but that requires moving the
133 * dict_proxy module one level down in the build dependency hierarchy.
134 */
135 #define CACHE_DICT_OPEN_FLAGS \
136 (DICT_FLAG_DUP_REPLACE | DICT_FLAG_SYNC_UPDATE | DICT_FLAG_UTF8_REQUEST)
137 #define PROXY_COLON DICT_TYPE_PROXY ":"
138 #define PROXY_COLON_LEN (sizeof(PROXY_COLON) - 1)
139
140 if (strncmp(map, PROXY_COLON, PROXY_COLON_LEN) != 0)
141 msg_fatal("SASL authentication cache name \"%s\" must start with \""
142 PROXY_COLON, map);
143
144 auth_cache = (SMTP_SASL_AUTH_CACHE *) mymalloc(sizeof(*auth_cache));
145 auth_cache->dict = dict_open(map, O_CREAT | O_RDWR, CACHE_DICT_OPEN_FLAGS);
146 auth_cache->ttl = ttl;
147 auth_cache->dsn = mystrdup("");
148 auth_cache->text = mystrdup("");
149 return (auth_cache);
150 }
151
152 /*
153 * Each cache lookup key contains a server host name and user name. Each
154 * cache value contains a time stamp, a hashed password, and the server
155 * response. With this organization, we don't have to worry about cache
156 * pollution, because we can detect if a cache entry has expired, or if the
157 * password has changed.
158 */
159
160 /* smtp_sasl_auth_cache_make_key - format auth failure cache lookup key */
161
smtp_sasl_auth_cache_make_key(const char * host,const char * user)162 static char *smtp_sasl_auth_cache_make_key(const char *host, const char *user)
163 {
164 VSTRING *buf = vstring_alloc(100);
165
166 vstring_sprintf(buf, "%s;%s", host, user);
167 return (vstring_export(buf));
168 }
169
170 /* smtp_sasl_auth_cache_make_pass - hash the auth failure cache password */
171
smtp_sasl_auth_cache_make_pass(const char * password)172 static char *smtp_sasl_auth_cache_make_pass(const char *password)
173 {
174 VSTRING *buf = vstring_alloc(2 * SHA_DIGEST_LENGTH);
175
176 base64_encode(buf, (const char *) SHA1((const unsigned char *) password,
177 strlen(password), 0),
178 SHA_DIGEST_LENGTH);
179 return (vstring_export(buf));
180 }
181
182 /* smtp_sasl_auth_cache_make_value - format auth failure cache value */
183
smtp_sasl_auth_cache_make_value(const char * password,const char * dsn,const char * rep_str)184 static char *smtp_sasl_auth_cache_make_value(const char *password,
185 const char *dsn,
186 const char *rep_str)
187 {
188 VSTRING *val_buf = vstring_alloc(100);
189 char *pwd_hash;
190 unsigned long now = (unsigned long) time((time_t *) 0);
191
192 pwd_hash = smtp_sasl_auth_cache_make_pass(password);
193 vstring_sprintf(val_buf, "%lu;%s;%s;%s", now, pwd_hash, dsn, rep_str);
194 myfree(pwd_hash);
195 return (vstring_export(val_buf));
196 }
197
198 /* smtp_sasl_auth_cache_valid_value - validate auth failure cache value */
199
smtp_sasl_auth_cache_valid_value(SMTP_SASL_AUTH_CACHE * auth_cache,const char * entry,const char * password)200 static int smtp_sasl_auth_cache_valid_value(SMTP_SASL_AUTH_CACHE *auth_cache,
201 const char *entry,
202 const char *password)
203 {
204 ssize_t len = strlen(entry);
205 char *cache_hash = mymalloc(len);
206 char *curr_hash;
207 unsigned long now = (unsigned long) time((time_t *) 0);
208 unsigned long time_stamp;
209 int valid;
210
211 auth_cache->dsn = myrealloc(auth_cache->dsn, len);
212 auth_cache->text = myrealloc(auth_cache->text, len);
213
214 if (sscanf(entry, "%lu;%[^;];%[^;];%[^\n]", &time_stamp, cache_hash,
215 auth_cache->dsn, auth_cache->text) != 4
216 || !dsn_valid(auth_cache->dsn)) {
217 msg_warn("bad smtp_sasl_auth_cache entry: %.100s", entry);
218 valid = 0;
219 } else if (time_stamp + auth_cache->ttl < now) {
220 valid = 0;
221 } else {
222 curr_hash = smtp_sasl_auth_cache_make_pass(password);
223 valid = (strcmp(cache_hash, curr_hash) == 0);
224 myfree(curr_hash);
225 }
226 myfree(cache_hash);
227 return (valid);
228 }
229
230 /* smtp_sasl_auth_cache_find - search auth failure cache */
231
smtp_sasl_auth_cache_find(SMTP_SASL_AUTH_CACHE * auth_cache,const SMTP_SESSION * session)232 int smtp_sasl_auth_cache_find(SMTP_SASL_AUTH_CACHE *auth_cache,
233 const SMTP_SESSION *session)
234 {
235 SMTP_ITERATOR *iter = session->iterator;
236 char *key;
237 const char *entry;
238 int valid = 0;
239
240 key = smtp_sasl_auth_cache_make_key(STR(iter->host), session->sasl_username);
241 if ((entry = dict_get(auth_cache->dict, key)) != 0)
242 if ((valid = smtp_sasl_auth_cache_valid_value(auth_cache, entry,
243 session->sasl_passwd)) == 0)
244 /* Remove expired, password changed, or malformed cache entry. */
245 if (dict_del(auth_cache->dict, key) != 0)
246 msg_warn("SASL auth failure map %s: entry not deleted: %s",
247 auth_cache->dict->name, key);
248 if (auth_cache->dict->error)
249 msg_warn("SASL auth failure map %s: lookup failed for %s",
250 auth_cache->dict->name, key);
251 myfree(key);
252 return (valid);
253 }
254
255 /* smtp_sasl_auth_cache_store - update auth failure cache */
256
smtp_sasl_auth_cache_store(SMTP_SASL_AUTH_CACHE * auth_cache,const SMTP_SESSION * session,const SMTP_RESP * resp)257 void smtp_sasl_auth_cache_store(SMTP_SASL_AUTH_CACHE *auth_cache,
258 const SMTP_SESSION *session,
259 const SMTP_RESP *resp)
260 {
261 SMTP_ITERATOR *iter = session->iterator;
262 char *key;
263 char *value;
264
265 key = smtp_sasl_auth_cache_make_key(STR(iter->host), session->sasl_username);
266 value = smtp_sasl_auth_cache_make_value(session->sasl_passwd,
267 resp->dsn, resp->str);
268 dict_put(auth_cache->dict, key, value);
269
270 myfree(value);
271 myfree(key);
272 }
273
274 #endif
275