6 #include "is_ascii.hpp"
13 #include <cppcodec/base32_crockford.hpp>
15 #include <arpa/inet.h>
18 #include <glog/logging.h>
20 #include <fmt/format.h>
21 #include <fmt/ostream.h>
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
[] = {
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 constexpr std::string_view REP_PREFIX
= "rep="; // legacy reply prefix
38 std::string
to_lower(std::string data
)
40 std::transform(data
.begin(), data
.end(), data
.begin(),
41 [](unsigned char c
) { return std::tolower(c
); });
45 static std::string
hash_rep(Reply::from_to
const& rep
, std::string_view secret
)
49 h
.update(to_lower(rep
.mail_from
));
50 h
.update(to_lower(rep
.rcpt_to_local_part
));
51 return to_lower(h
.final().substr(0, hash_length_min
));
54 std::string
enc_reply_blob(Reply::from_to
const& rep
, std::string_view secret
)
56 auto const hash
= hash_rep(rep
, secret
);
58 auto const pkt
= fmt::format("{}{}{}{}{}", // clang-format off
60 rep
.rcpt_to_local_part
, '\0',
61 rep
.mail_from
); // clang-format on
63 return to_lower(cppcodec::base32_crockford::encode(pkt
));
66 std::string
Reply::enc_reply(Reply::from_to
const& rep
, std::string_view secret
)
68 auto const result
= Mailbox::parse(rep
.mail_from
);
70 throw std::invalid_argument("invalid mailbox syntax in enc_reply");
73 // If it's "local part"@example.com or local-part@[127.0.0.1] we
74 // must fall back to the blob style.
75 if (result
->local_type
== Mailbox::local_types::quoted_string
||
76 result
->domain_type
== Mailbox::domain_types::address_literal
) {
77 return enc_reply_blob(rep
, secret
);
81 Mailbox::parse(fmt::format("{}@x.y", rep
.rcpt_to_local_part
));
83 throw std::invalid_argument("invalid local-part syntax in enc_reply");
85 if (rcpt_to
->local_type
== Mailbox::local_types::quoted_string
) {
86 return enc_reply_blob(rep
, secret
);
89 for (auto sep_char
: sep_chars
) {
90 if (rep
.rcpt_to_local_part
.find(sep_char
) == std::string_view::npos
) {
91 // Must never be in the domain part, that's crazy
92 CHECK_EQ(result
->domain
.find(sep_char
), std::string_view::npos
);
93 // The sep_char *can* be in the result->local part
94 auto const hash_enc
= hash_rep(rep
, secret
);
95 return fmt::format("{}{}{}{}{}{}{}", // clang-format off
96 result
->local
, sep_char
,
97 result
->domain
, sep_char
,
98 rep
.rcpt_to_local_part
, sep_char
,
99 hash_enc
); // clang-format on
103 return enc_reply_blob(rep
, secret
);
106 auto split(std::string
const& str
, const char delim
)
108 std::vector
<std::string
> out
;
112 while ((start
= str
.find_first_not_of(delim
, end
)) != std::string::npos
) {
113 end
= str
.find(delim
, start
);
114 out
.push_back(str
.substr(start
, end
- start
));
120 static std::optional
<Reply::from_to
> dec_reply_blob(std::string_view addr
,
121 std::string_view secret
)
123 auto const pktv
= cppcodec::base32_crockford::decode(addr
);
125 std::string(reinterpret_cast<char const*>(pktv
.data()), pktv
.size());
127 auto const parts
= split(pkt
, '\0');
129 if (parts
.size() != 3) {
130 LOG(WARNING
) << "invalid blob format";
134 auto const hash
= parts
[0];
137 rep
.rcpt_to_local_part
= parts
[1];
138 rep
.mail_from
= parts
[2];
140 auto const hash_computed
= hash_rep(rep
, secret
);
142 if (!iequal(hash_computed
, hash
)) {
143 LOG(WARNING
) << "hash check failed";
150 static bool is_pure_base32(std::string_view s
)
153 static constexpr const char base32_crockford_alphabet_i
[] = {
154 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
155 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
158 'P', 'Q', 'R', 'S', 'T',
159 'V', 'W', 'X', 'Y', 'Z',
160 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
163 'p', 'q', 'r', 's', 't',
164 'v', 'w', 'x', 'y', 'z'
168 auto constexpr alpha
= std::string_view(base32_crockford_alphabet_i
,
169 sizeof(base32_crockford_alphabet_i
));
171 // If we can't find anything not in the base32 alphabet, it's pure
172 return s
.find_first_not_of(alpha
) == std::string_view::npos
;
175 std::optional
<Reply::from_to
>
176 try_decode(std::string_view addr
, std::string_view secret
, char sep_char
)
178 // {mail_from.local}={mail_from.domain}={rcpt_to_local_part}={hash}
180 auto const hash_sep
= addr
.find_last_of(sep_char
);
181 if (hash_sep
== std::string_view::npos
)
183 auto const hash_pos
= hash_sep
+ 1;
184 auto const hash_len
= addr
.length() - hash_pos
;
185 if ((hash_len
< hash_length_min
) || (hash_len
> hash_length_max
))
187 auto const hash
= addr
.substr(hash_pos
, hash_len
);
189 // The hash part must look like a hash
190 if (!is_pure_base32(hash
))
193 auto const rcpt_loc_sep
= addr
.substr(0, hash_sep
).find_last_of(sep_char
);
194 if (rcpt_loc_sep
== std::string_view::npos
)
196 auto const rcpt_loc_pos
= rcpt_loc_sep
+ 1;
197 auto const rcpt_loc_len
= hash_sep
- rcpt_loc_pos
;
198 auto const rcpt_loc
= addr
.substr(rcpt_loc_pos
, rcpt_loc_len
);
200 auto const mail_from_dom_sep
=
201 addr
.substr(0, rcpt_loc_sep
).find_last_of(sep_char
);
202 if (mail_from_dom_sep
== std::string_view::npos
)
204 auto const mail_from_dom_pos
= mail_from_dom_sep
+ 1;
205 auto const mail_from_dom_len
= rcpt_loc_sep
- mail_from_dom_pos
;
206 auto const mail_from_dom
= addr
.substr(mail_from_dom_pos
, mail_from_dom_len
);
208 auto const mail_from_loc
= addr
.substr(0, mail_from_dom_sep
);
209 auto const mail_from
= fmt::format("{}@{}", mail_from_loc
, mail_from_dom
);
211 // The mail_from part must be a valid Mailbox address.
212 if (!Mailbox::validate(mail_from
))
216 rep
.mail_from
= mail_from
;
217 rep
.rcpt_to_local_part
= rcpt_loc
;
219 auto const hash_computed
= hash_rep(rep
, secret
);
221 if (!iequal(hash_computed
, hash
)) {
222 LOG(WARNING
) << "hash check failed";
230 * Legacy format reply address with the REP= prefix. We no longer
231 * generates these addresses, but we continue to decode them in a
235 std::optional
<Reply::from_to
> old_dec_reply(std::string_view addr
,
236 std::string_view secret
)
238 addr
.remove_prefix(REP_PREFIX
.length());
240 if (is_pure_base32(addr
)) {
241 // if everything after REP= is base32 we have a blob
242 return dec_reply_blob(addr
, secret
);
245 // REP= has been removed, addr is now:
246 // {hash}={rcpt_to_local_part}={mail_from.local}={mail_from.domain}
248 // and mail_from.local can contain '=' chars
250 auto const first_sep
= addr
.find_first_of('=');
251 auto const last_sep
= addr
.find_last_of('=');
252 auto const second_sep
= addr
.find_first_of('=', first_sep
+ 1);
254 if (first_sep
== last_sep
|| second_sep
== last_sep
) {
255 LOG(WARNING
) << "unrecognized legacy reply format " << addr
;
259 auto const rcpt_to_pos
= first_sep
+ 1;
260 auto const mf_loc_pos
= second_sep
+ 1;
261 auto const mf_dom_pos
= last_sep
+ 1;
263 auto const rcpt_to_len
= second_sep
- rcpt_to_pos
;
264 auto const mf_loc_len
= last_sep
- mf_loc_pos
;
266 auto const reply_hash
= addr
.substr(0, first_sep
);
267 auto const rcpt_to_loc
= addr
.substr(rcpt_to_pos
, rcpt_to_len
);
268 auto const mail_from_loc
= addr
.substr(mf_loc_pos
, mf_loc_len
);
269 auto const mail_from_dom
= addr
.substr(mf_dom_pos
, std::string_view::npos
);
272 rep
.rcpt_to_local_part
= rcpt_to_loc
;
273 rep
.mail_from
= fmt::format("{}@{}", mail_from_loc
, mail_from_dom
);
275 auto const hash_enc
= hash_rep(rep
, secret
);
277 if (!iequal(reply_hash
, hash_enc
)) {
284 std::optional
<Reply::from_to
> Reply::dec_reply(std::string_view addr
,
285 std::string_view secret
)
287 // Check for legacy format, process appropriately.
288 if (istarts_with(addr
, REP_PREFIX
)) {
289 return old_dec_reply(addr
, secret
);
292 auto const addr_mbx
= Mailbox::parse(fmt::format("{}@x.y", addr
));
294 throw std::invalid_argument("invalid address syntax in dec_reply");
297 // The blob for the address <"x"@y.z> is 26 bytes long.
298 if (is_pure_base32(addr
)) {
299 // if everything is base32 we might have a blob
300 if (addr
.length() > 25) {
301 return dec_reply_blob(addr
, secret
);
303 return {}; // normal local-part
306 for (auto sep_char
: sep_chars
) {
307 auto const rep
= try_decode(addr
, secret
, sep_char
);
312 LOG(WARNING
) << "not a reply address: " << addr
;