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
111SMTP_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
162static 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
172static 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
184static 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
200static 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
232int     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
257void    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