/*	$OpenBSD: srs.c,v 1.3 2019/09/29 10:03:49 gilles Exp $	*/

/*
 * Copyright (c) 2019 Gilles Chehade <gilles@poolp.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "includes.h"

#include <sys/types.h>
#include <sys/queue.h>
#include <sys/tree.h>
#include <sys/socket.h>

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <event.h>
#include <imsg.h>
#include <inttypes.h>
#include <netdb.h>
#include <limits.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include <openssl/sha.h>

#include "smtpd.h"
#include "log.h"

static uint8_t	base32[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

static int
minrange(uint16_t tref, uint16_t t2, int drift, int mod)
{
	if (tref > drift) {
		/* t2 must fall in between tref and tref - drift */
		if (t2 <= tref && t2>= tref - drift)
			return 1;
	}
	else {
		/* t2 must fall in between 0 and tref, or wrap */
		if (t2 <= tref || t2 >= mod - (drift - tref))
			return 1;
	}
	return 0;
}

static int
maxrange(uint16_t tref, uint16_t t2, int drift, int mod)
{
	if (tref + drift < 1024) {
		/* t2 must fall in between tref and tref + drift */
		if (t2 >= tref && t2 <= tref + drift)
			return 1;
	}
	else {
		/* t2 must fall in between tref + drift, or wrap */
		if (t2 >= tref || t2 <= (tref + drift) % 1024)
			return 1;
	}
	return 0;
}

static int
timestamp_check_range(uint16_t tref, uint16_t t2)
{
	if (! minrange(tref, t2, env->sc_srs_ttl, 1024) &&
	    ! maxrange(tref, t2, 1, 1024))
		return 0;

	return 1;
}

static const unsigned char *
srs_hash(const char *key, const char *value)
{
	SHA_CTX	c;
	static unsigned char md[SHA_DIGEST_LENGTH];

	SHA1_Init(&c);
	SHA1_Update(&c, key, strlen(key));
	SHA1_Update(&c, value, strlen(value));
	SHA1_Final(md, &c);
	return md;
}

static const char *
srs0_encode(const char *sender, const char *rcpt_domain)
{
	static char dest[SMTPD_MAXMAILADDRSIZE];
	char tmp[SMTPD_MAXMAILADDRSIZE];
	char md[SHA_DIGEST_LENGTH*4+1];
	struct mailaddr maddr;
	uint16_t timestamp;
	int ret;

	/* compute 10 bits timestamp according to spec */
	timestamp = (time(NULL) / (60 * 60 * 24)) % 1024;

	/* parse sender into user and domain */
	if (! text_to_mailaddr(&maddr, sender))
		return sender;

	/* TT=<orig_domainpart>=<orig_userpart>@<new_domainpart> */
	ret = snprintf(tmp, sizeof tmp, "%c%c=%s=%s@%s",
	    base32[(timestamp>>5) & 0x1F],
	    base32[timestamp & 0x1F],
	    maddr.domain, maddr.user, rcpt_domain);
	if (ret == -1 || ret >= (int)sizeof tmp)
		return sender;

	/* compute HHHH */
	base64_encode_rfc3548(srs_hash(env->sc_srs_key, tmp), SHA_DIGEST_LENGTH,
	    md, sizeof md);

	/* prepend SRS0=HHHH= prefix */
	ret = snprintf(dest, sizeof dest, "SRS0=%c%c%c%c=%s",
	    md[0], md[1], md[2], md[3], tmp);
	if (ret == -1 || ret >= (int)sizeof dest)
		return sender;

	return dest;
}

static const char *
srs1_encode_srs0(const char *sender, const char *rcpt_domain)
{
	static char dest[SMTPD_MAXMAILADDRSIZE];
	char tmp[SMTPD_MAXMAILADDRSIZE];
	char md[SHA_DIGEST_LENGTH*4+1];
	struct mailaddr maddr;
	int ret;

	/* parse sender into user and domain */
	if (! text_to_mailaddr(&maddr, sender))
		return sender;

	/* <last_domainpart>==<SRS0_userpart>@<new_domainpart> */
	ret = snprintf(tmp, sizeof tmp, "%s==%s@%s",
	    maddr.domain, maddr.user, rcpt_domain);
	if (ret == -1 || ret >= (int)sizeof tmp)
		return sender;

	/* compute HHHH */
	base64_encode_rfc3548(srs_hash(env->sc_srs_key, tmp), SHA_DIGEST_LENGTH,
		md, sizeof md);

	/* prepend SRS1=HHHH= prefix */
	ret = snprintf(dest, sizeof dest, "SRS1=%c%c%c%c=%s",
	    md[0], md[1], md[2], md[3], tmp);
	if (ret == -1 || ret >= (int)sizeof dest)
		return sender;

	return dest;
}

static const char *
srs1_encode_srs1(const char *sender, const char *rcpt_domain)
{
	static char dest[SMTPD_MAXMAILADDRSIZE];
	char tmp[SMTPD_MAXMAILADDRSIZE];
	char md[SHA_DIGEST_LENGTH*4+1];
	struct mailaddr maddr;
	int ret;

	/* parse sender into user and domain */
	if (! text_to_mailaddr(&maddr, sender))
		return sender;

	/* <SRS1_userpart>@<new_domainpart> */
	ret = snprintf(tmp, sizeof tmp, "%s@%s", maddr.user, rcpt_domain);
	if (ret == -1 || ret >= (int)sizeof tmp)
		return sender;

	/* sanity check: there's at least room for a checksum
	 * with allowed delimiter =, + or -
	 */
	if (strlen(tmp) < 5)
		return sender;
	if (tmp[4] != '=' && tmp[4] != '+' && tmp[4] != '-')
		return sender;

	/* compute HHHH */
	base64_encode_rfc3548(srs_hash(env->sc_srs_key, tmp + 5), SHA_DIGEST_LENGTH,
		md, sizeof md);

	/* prepend SRS1=HHHH= prefix skipping previous hops' HHHH */
	ret = snprintf(dest, sizeof dest, "SRS1=%c%c%c%c=%s",
	    md[0], md[1], md[2], md[3], tmp + 5);
	if (ret == -1 || ret >= (int)sizeof dest)
		return sender;

	return dest;
}

const char *
srs_encode(const char *sender, const char *rcpt_domain)
{
	if (strncasecmp(sender, "SRS0=", 5) == 0)
		return srs1_encode_srs0(sender+5, rcpt_domain);
	if (strncasecmp(sender, "SRS1=", 5) == 0)
		return srs1_encode_srs1(sender+5, rcpt_domain);
	return srs0_encode(sender, rcpt_domain);
}

static const char *
srs0_decode(const char *rcpt)
{
	static char dest[SMTPD_MAXMAILADDRSIZE];
	char md[SHA_DIGEST_LENGTH*4+1];
	struct mailaddr maddr;
	char *p;
	uint8_t *idx;
	int ret;
	uint16_t timestamp, srs_timestamp;

	/* sanity check: we have room for a checksum and delimiter */
	if (strlen(rcpt) < 5)
		return NULL;

	/* compute checksum */
	base64_encode_rfc3548(srs_hash(env->sc_srs_key, rcpt+5), SHA_DIGEST_LENGTH,
	    md, sizeof md);

	/* compare prefix checksum with computed checksum */
	if (strncmp(md, rcpt, 4) != 0) {
		if (env->sc_srs_key_backup == NULL)
			return NULL;
		base64_encode_rfc3548(srs_hash(env->sc_srs_key_backup, rcpt+5),
		    SHA_DIGEST_LENGTH, md, sizeof md);
		if (strncmp(md, rcpt, 4) != 0)
			return NULL;
	}
	rcpt += 5;

	/* sanity check: we have room for a timestamp and delimiter */
	if (strlen(rcpt) < 3)
		return NULL;

	/* decode timestamp */
	if ((idx = strchr(base32, rcpt[0])) == NULL)
		return NULL;
	srs_timestamp = ((idx - base32) << 5);

	if ((idx = strchr(base32, rcpt[1])) == NULL)
		return NULL;
	srs_timestamp |= (idx - base32);
	rcpt += 3;

	/* compute current 10 bits timestamp */
	timestamp = (time(NULL) / (60 * 60 * 24)) % 1024;

	/* check that SRS timestamp isn't too far from current */
	if (timestamp != srs_timestamp)
		if (! timestamp_check_range(timestamp, srs_timestamp))
			return NULL;

	if (! text_to_mailaddr(&maddr, rcpt))
		return NULL;

	/* sanity check: we have at least one SRS separator */
	if ((p = strchr(maddr.user, '=')) == NULL)
		return NULL;
	*p++ = '\0';

	/* maddr.user holds "domain\0user", with p pointing at user */
	ret = snprintf(dest, sizeof dest, "%s@%s", p, maddr.user);
	if (ret == -1 || ret >= (int)sizeof dest)
		return NULL;

	return dest;
}

static const char *
srs1_decode(const char *rcpt)
{
	static char dest[SMTPD_MAXMAILADDRSIZE];
	char md[SHA_DIGEST_LENGTH*4+1];
	struct mailaddr maddr;
	char *p;
	uint8_t *idx;
	int ret;
	uint16_t timestamp, srs_timestamp;

	/* sanity check: we have room for a checksum and delimiter */
	if (strlen(rcpt) < 5)
		return NULL;

	/* compute checksum */
	base64_encode_rfc3548(srs_hash(env->sc_srs_key, rcpt+5), SHA_DIGEST_LENGTH,
	    md, sizeof md);

	/* compare prefix checksum with computed checksum */
	if (strncmp(md, rcpt, 4) != 0) {
		if (env->sc_srs_key_backup == NULL)
			return NULL;
		base64_encode_rfc3548(srs_hash(env->sc_srs_key_backup, rcpt+5),
		    SHA_DIGEST_LENGTH, md, sizeof md);
		if (strncmp(md, rcpt, 4) != 0)
			return NULL;
	}
	rcpt += 5;

	if (! text_to_mailaddr(&maddr, rcpt))
		return NULL;

	/* sanity check: we have at least one SRS separator */
	if ((p = strchr(maddr.user, '=')) == NULL)
		return NULL;
	*p++ = '\0';

	/* maddr.user holds "domain\0user", with p pointing at user */
	ret = snprintf(dest, sizeof dest, "SRS0%s@%s", p, maddr.user);
	if (ret == -1 || ret >= (int)sizeof dest)
		return NULL;


	/* we're ready to return decoded address, but let's check if
	 * SRS0 timestamp is valid.
	 */

	/* first, get rid of SRS0 checksum (=HHHH=), we can't check it */
	if (strlen(p) < 6)
		return NULL;
	p += 6;

	/* we should be pointing to a timestamp, check that we're indeed */
	if (strlen(p) < 3)
		return NULL;
	if (p[2] != '=' && p[2] != '+' && p[2] != '-')
		return NULL;
	p[2] = '\0';

	if ((idx = strchr(base32, p[0])) == NULL)
		return NULL;
	srs_timestamp = ((idx - base32) << 5);

	if ((idx = strchr(base32, p[1])) == NULL)
		return NULL;
	srs_timestamp |= (idx - base32);

	/* compute current 10 bits timestamp */
	timestamp = (time(NULL) / (60 * 60 * 24)) % 1024;

	/* check that SRS timestamp isn't too far from current */
	if (timestamp != srs_timestamp)
		if (! timestamp_check_range(timestamp, srs_timestamp))
			return NULL;

	return dest;
}

const char *
srs_decode(const char *rcpt)
{
	if (strncasecmp(rcpt, "SRS0=", 5) == 0)
		return srs0_decode(rcpt + 5);
	if (strncasecmp(rcpt, "SRS1=", 5) == 0)
		return srs1_decode(rcpt + 5);

	return NULL;
}
