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_bytes_reply
= 6;
28 constexpr std::string_view REP_PREFIX
= "rep=";
30 constexpr char sep_char
= '='; // must match above *_PREFIX values
32 std::string
to_lower(std::string data
)
34 std::transform(data
.begin(), data
.end(), data
.begin(),
35 [](unsigned char c
) { return std::tolower(c
); });
39 static std::string
hash_rep(Reply::from_to
const& rep
, std::string_view secret
)
43 h
.update(to_lower(rep
.mail_from
));
44 h
.update(to_lower(rep
.rcpt_to_local_part
));
45 return h
.final().substr(0, hash_bytes_reply
);
48 std::string
enc_reply_blob(Reply::from_to
const& rep
, std::string_view secret
)
50 auto const hash
= hash_rep(rep
, secret
);
52 auto const pkt
= fmt::format("{}{}{}{}{}", // clang-format off
54 rep
.rcpt_to_local_part
, '\0',
55 rep
.mail_from
); // clang-format on
57 return fmt::format("{}{}", REP_PREFIX
,
58 cppcodec::base32_crockford::encode(pkt
));
61 std::string
Reply::enc_reply(Reply::from_to
const& rep
, std::string_view secret
)
63 auto const result
= Mailbox::parse(rep
.mail_from
);
65 throw std::invalid_argument("invalid mailbox syntax in enc_reply");
68 // If it's "local part"@example.com or local-part@[127.0.0.1] we
69 // must fall back to the blob style.
70 if (result
->local_type
== Mailbox::local_types::quoted_string
||
71 result
->domain_type
== Mailbox::domain_types::address_literal
) {
72 return enc_reply_blob(rep
, secret
);
75 // If rcpt_to_local_part contain a '=' fall back.
76 if (rep
.rcpt_to_local_part
.find(sep_char
) != std::string_view::npos
) {
77 return enc_reply_blob(rep
, secret
);
80 auto const mail_from
= Mailbox(rep
.mail_from
);
82 auto const hash_enc
= hash_rep(rep
, secret
);
84 return fmt::format("{}{}{}{}{}{}{}{}", // clang-format off
85 REP_PREFIX
, // includes sep_char
87 rep
.rcpt_to_local_part
, sep_char
,
88 mail_from
.local_part(), sep_char
,
89 mail_from
.domain().utf8());
93 auto split(std::string
const& str
, const char delim
)
95 std::vector
<std::string
> out
;
99 while ((start
= str
.find_first_not_of(delim
, end
)) != std::string::npos
) {
100 end
= str
.find(delim
, start
);
101 out
.push_back(str
.substr(start
, end
- start
));
107 static std::optional
<Reply::from_to
> dec_reply_blob(std::string_view addr
,
108 std::string_view secret
)
110 auto const pktv
= cppcodec::base32_crockford::decode(addr
);
112 std::string(reinterpret_cast<char const*>(pktv
.data()), pktv
.size());
114 auto const parts
= split(pkt
, '\0');
116 auto const hash
= parts
[0];
119 rep
.rcpt_to_local_part
= parts
[1];
120 rep
.mail_from
= parts
[2];
122 auto const hash_computed
= hash_rep(rep
, secret
);
124 if (!iequal(hash_computed
, hash
)) {
125 LOG(WARNING
) << "hash check failed";
132 static bool is_pure_base32(std::string_view s
)
134 auto constexpr alpha
=
135 std::string_view(cppcodec::detail::base32_crockford_alphabet
,
136 sizeof(cppcodec::detail::base32_crockford_alphabet
));
137 // If we can't find anything not in the base32 alphabet, it's pure
138 return s
.find_first_not_of(alpha
) == std::string_view::npos
;
141 std::optional
<Reply::from_to
> Reply::dec_reply(std::string_view addr
,
142 std::string_view secret
)
144 if (!istarts_with(addr
, REP_PREFIX
)) {
145 LOG(WARNING
) << addr
<< " not a valid reply address";
148 addr
.remove_prefix(REP_PREFIX
.length());
150 if (is_pure_base32(addr
)) {
151 // if everything after REP= is base32 we have a blob
152 return dec_reply_blob(addr
, secret
);
155 // REP= has been removed, addr is now:
156 // {hash}={rcpt_to_local_part}={mail_from.local}={mail_from.domain}
158 // and mail_from.local can contain '=' chars
160 auto const first_sep
= addr
.find_first_of(sep_char
);
161 auto const last_sep
= addr
.find_last_of(sep_char
);
162 auto const second_sep
= addr
.find_first_of(sep_char
, first_sep
+ 1);
164 if (first_sep
== last_sep
|| second_sep
== last_sep
) {
165 LOG(WARNING
) << "unrecognized reply format " << addr
;
169 auto const rcpt_to_pos
= first_sep
+ 1;
170 auto const mf_loc_pos
= second_sep
+ 1;
171 auto const mf_dom_pos
= last_sep
+ 1;
173 auto const rcpt_to_len
= second_sep
- rcpt_to_pos
;
174 auto const mf_loc_len
= last_sep
- mf_loc_pos
;
176 auto const reply_hash
= addr
.substr(0, first_sep
);
177 auto const rcpt_to_loc
= addr
.substr(rcpt_to_pos
, rcpt_to_len
);
178 auto const mail_from_loc
= addr
.substr(mf_loc_pos
, mf_loc_len
);
179 auto const mail_from_dom
= addr
.substr(mf_dom_pos
, std::string_view::npos
);
182 rep
.rcpt_to_local_part
= rcpt_to_loc
;
183 rep
.mail_from
= fmt::format("{}@{}", mail_from_loc
, mail_from_dom
);
185 auto const hash_enc
= hash_rep(rep
, secret
);
187 if (!iequal(reply_hash
, hash_enc
)) {
188 LOG(WARNING
) << "hash mismatch in reply " << addr
;
189 LOG(WARNING
) << " reply_hash == " << reply_hash
;
190 LOG(WARNING
) << " hash_enc == " << hash_enc
;
191 LOG(WARNING
) << " rcpt_to_loc == " << rcpt_to_loc
;
192 LOG(WARNING
) << "mail_from_loc == " << mail_from_loc
;
193 LOG(WARNING
) << "mail_from_dom == " << mail_from_dom
;