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 std::string
to_lower(std::string data
)
38 std::transform(data
.begin(), data
.end(), data
.begin(),
39 [](unsigned char c
) { return std::tolower(c
); });
43 static std::string
hash_rep(Reply::from_to
const& rep
, std::string_view 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
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
);
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
;
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
));
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
);
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";
123 auto const hash
= parts
[0];
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";
139 static bool is_pure_base32(std::string_view s
)
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',
147 'P', 'Q', 'R', 'S', 'T',
148 'V', 'W', 'X', 'Y', 'Z',
149 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
152 'p', 'q', 'r', 's', 't',
153 'v', 'w', 'x', 'y', 'z'
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
)
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
))
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
))
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
)
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
)
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
))
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";
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
);
236 LOG(WARNING
) << "not a reply address: " << addr
;