another test case
[ghsmtp.git] / Reply.cpp
blobd3cda6860b732e6e0b2db1283464c1e431e59da8
1 #include "Reply.hpp"
3 #include "Hash.hpp"
4 #include "Mailbox.hpp"
5 #include "iequal.hpp"
6 #include "is_ascii.hpp"
8 #include <algorithm>
9 #include <cctype>
10 #include <iterator>
11 #include <string>
13 #include <cppcodec/base32_crockford.hpp>
15 #include <arpa/inet.h>
16 #include <time.h>
18 #include <glog/logging.h>
20 #include <fmt/format.h>
21 #include <fmt/ostream.h>
23 using std::begin;
24 using std::end;
26 constexpr int hash_length_min = 6; // 1 in a billion
27 constexpr int hash_length_max = 10;
29 constexpr const char sep_chars_array[] = {
30 '_',
31 '=' // Must not be allowed in domain names, must not be in base32 alphabet.
34 constexpr std::string_view sep_chars{sep_chars_array, sizeof(sep_chars_array)};
36 std::string to_lower(std::string data)
38 std::transform(data.begin(), data.end(), data.begin(),
39 [](unsigned char c) { return std::tolower(c); });
40 return data;
43 static std::string hash_rep(Reply::from_to const& rep, std::string_view secret)
45 Hash h;
46 h.update(secret);
47 h.update(to_lower(rep.mail_from));
48 h.update(to_lower(rep.rcpt_to_local_part));
49 return to_lower(h.final().substr(0, hash_length_min));
52 std::string enc_reply_blob(Reply::from_to const& rep, std::string_view secret)
54 auto const hash = hash_rep(rep, secret);
56 auto const pkt = fmt::format("{}{}{}{}{}", // clang-format off
57 hash, '\0',
58 rep.rcpt_to_local_part, '\0',
59 rep.mail_from); // clang-format on
61 return to_lower(cppcodec::base32_crockford::encode(pkt));
64 std::string Reply::enc_reply(Reply::from_to const& rep, std::string_view secret)
66 auto const result = Mailbox::parse(rep.mail_from);
67 if (!result) {
68 throw std::invalid_argument("invalid mailbox syntax in enc_reply");
71 // If it's "local part"@example.com or local-part@[127.0.0.1] we
72 // must fall back to the blob style.
73 if (result->local_type == Mailbox::local_types::quoted_string ||
74 result->domain_type == Mailbox::domain_types::address_literal) {
75 return enc_reply_blob(rep, secret);
78 for (auto sep_char : sep_chars) {
79 if (rep.rcpt_to_local_part.find(sep_char) == std::string_view::npos) {
80 // Must never be in the domain part, that's crazy
81 CHECK_EQ(result->domain.find(sep_char), std::string_view::npos);
82 // The sep_char *can* be in the result->local part
83 auto const hash_enc = hash_rep(rep, secret);
84 return fmt::format("{}{}{}{}{}{}{}", // clang-format off
85 result->local, sep_char,
86 result->domain, sep_char,
87 rep.rcpt_to_local_part, sep_char,
88 hash_enc); // clang-format on
92 return enc_reply_blob(rep, secret);
95 auto split(std::string const& str, const char delim)
97 std::vector<std::string> out;
99 size_t start;
100 size_t end = 0;
101 while ((start = str.find_first_not_of(delim, end)) != std::string::npos) {
102 end = str.find(delim, start);
103 out.push_back(str.substr(start, end - start));
106 return out;
109 static std::optional<Reply::from_to> dec_reply_blob(std::string_view addr,
110 std::string_view secret)
112 auto const pktv = cppcodec::base32_crockford::decode(addr);
113 auto const pkt =
114 std::string(reinterpret_cast<char const*>(pktv.data()), pktv.size());
116 auto const parts = split(pkt, '\0');
118 if (parts.size() != 3) {
119 LOG(WARNING) << "invalid blob format";
120 return {};
123 auto const hash = parts[0];
125 Reply::from_to rep;
126 rep.rcpt_to_local_part = parts[1];
127 rep.mail_from = parts[2];
129 auto const hash_computed = hash_rep(rep, secret);
131 if (!iequal(hash_computed, hash)) {
132 LOG(WARNING) << "hash check failed";
133 return {};
136 return rep;
139 static bool is_pure_base32(std::string_view s)
141 // clang-format off
142 static constexpr const char base32_crockford_alphabet_i[] = {
143 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
144 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
145 'J', 'K',
146 'M', 'N',
147 'P', 'Q', 'R', 'S', 'T',
148 'V', 'W', 'X', 'Y', 'Z',
149 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
150 'j', 'k',
151 'm', 'n',
152 'p', 'q', 'r', 's', 't',
153 'v', 'w', 'x', 'y', 'z'
155 // clang-format on
157 auto constexpr alpha = std::string_view(base32_crockford_alphabet_i,
158 sizeof(base32_crockford_alphabet_i));
160 // If we can't find anything not in the base32 alphabet, it's pure
161 return s.find_first_not_of(alpha) == std::string_view::npos;
164 std::optional<Reply::from_to>
165 try_decode(std::string_view addr, std::string_view secret, char sep_char)
167 // {mail_from.local}={mail_from.domain}={rcpt_to_local_part}={hash}
169 auto const hash_sep = addr.find_last_of(sep_char);
170 if (hash_sep == std::string_view::npos)
171 return {};
172 auto const hash_pos = hash_sep + 1;
173 auto const hash_len = addr.length() - hash_pos;
174 if ((hash_len < hash_length_min) || (hash_len > hash_length_max))
175 return {};
176 auto const hash = addr.substr(hash_pos, hash_len);
178 // The hash part must look like a hash
179 if (!is_pure_base32(hash))
180 return {};
182 auto const rcpt_loc_sep = addr.substr(0, hash_sep).find_last_of(sep_char);
183 if (rcpt_loc_sep == std::string_view::npos)
184 return {};
185 auto const rcpt_loc_pos = rcpt_loc_sep + 1;
186 auto const rcpt_loc_len = hash_sep - rcpt_loc_pos;
187 auto const rcpt_loc = addr.substr(rcpt_loc_pos, rcpt_loc_len);
189 auto const mail_from_dom_sep =
190 addr.substr(0, rcpt_loc_sep).find_last_of(sep_char);
191 if (mail_from_dom_sep == std::string_view::npos)
192 return {};
193 auto const mail_from_dom_pos = mail_from_dom_sep + 1;
194 auto const mail_from_dom_len = rcpt_loc_sep - mail_from_dom_pos;
195 auto const mail_from_dom = addr.substr(mail_from_dom_pos, mail_from_dom_len);
197 auto const mail_from_loc = addr.substr(0, mail_from_dom_sep);
198 auto const mail_from = fmt::format("{}@{}", mail_from_loc, mail_from_dom);
200 // The mail_from part must be a valid Mailbox address.
201 if (!Mailbox::validate(mail_from))
202 return {};
204 Reply::from_to rep;
205 rep.mail_from = mail_from;
206 rep.rcpt_to_local_part = rcpt_loc;
208 auto const hash_computed = hash_rep(rep, secret);
210 if (!iequal(hash_computed, hash)) {
211 LOG(WARNING) << "hash check failed";
212 return {};
215 return rep;
218 std::optional<Reply::from_to> Reply::dec_reply(std::string_view addr,
219 std::string_view secret)
221 // The blob for the address <"x"@y.z> is 26 bytes long.
222 if (is_pure_base32(addr)) {
223 // if everything is base32 we might have a blob
224 if (addr.length() > 25) {
225 return dec_reply_blob(addr, secret);
227 return {}; // normal local-part
230 for (auto sep_char : sep_chars) {
231 auto const rep = try_decode(addr, secret, sep_char);
232 if (rep)
233 return rep;
236 LOG(WARNING) << "not a reply address: " << addr;
238 return {};