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