1 | // |
2 | // Database.cpp |
3 | // |
4 | // Library: MongoDB |
5 | // Package: MongoDB |
6 | // Module: Database |
7 | // |
8 | // Copyright (c) 2012, Applied Informatics Software Engineering GmbH. |
9 | // and Contributors. |
10 | // |
11 | // SPDX-License-Identifier: BSL-1.0 |
12 | // |
13 | |
14 | |
15 | #include "Poco/MongoDB/Database.h" |
16 | #include "Poco/MongoDB/Binary.h" |
17 | #include "Poco/MD5Engine.h" |
18 | #include "Poco/SHA1Engine.h" |
19 | #include "Poco/PBKDF2Engine.h" |
20 | #include "Poco/HMACEngine.h" |
21 | #include "Poco/Base64Decoder.h" |
22 | #include "Poco/MemoryStream.h" |
23 | #include "Poco/StreamCopier.h" |
24 | #include "Poco/Exception.h" |
25 | #include "Poco/RandomStream.h" |
26 | #include "Poco/Random.h" |
27 | #include "Poco/Format.h" |
28 | #include "Poco/NumberParser.h" |
29 | #include <sstream> |
30 | #include <map> |
31 | |
32 | |
33 | namespace Poco { |
34 | namespace MongoDB { |
35 | |
36 | |
37 | const std::string Database::AUTH_MONGODB_CR("MONGODB-CR" ); |
38 | const std::string Database::AUTH_SCRAM_SHA1("SCRAM-SHA-1" ); |
39 | |
40 | |
41 | namespace |
42 | { |
43 | std::map<std::string, std::string> parseKeyValueList(const std::string& str) |
44 | { |
45 | std::map<std::string, std::string> kvm; |
46 | std::string::const_iterator it = str.begin(); |
47 | std::string::const_iterator end = str.end(); |
48 | while (it != end) |
49 | { |
50 | std::string k; |
51 | std::string v; |
52 | while (it != end && *it != '=') k += *it++; |
53 | if (it != end) ++it; |
54 | while (it != end && *it != ',') v += *it++; |
55 | if (it != end) ++it; |
56 | kvm[k] = v; |
57 | } |
58 | return kvm; |
59 | } |
60 | |
61 | std::string decodeBase64(const std::string& base64) |
62 | { |
63 | Poco::MemoryInputStream istr(base64.data(), base64.size()); |
64 | Poco::Base64Decoder decoder(istr); |
65 | std::string result; |
66 | Poco::StreamCopier::copyToString(decoder, result); |
67 | return result; |
68 | } |
69 | |
70 | std::string encodeBase64(const std::string& data) |
71 | { |
72 | std::ostringstream ostr; |
73 | Poco::Base64Encoder encoder(ostr); |
74 | encoder.rdbuf()->setLineLength(0); |
75 | encoder << data; |
76 | encoder.close(); |
77 | return ostr.str(); |
78 | } |
79 | |
80 | std::string digestToBinaryString(Poco::DigestEngine& engine) |
81 | { |
82 | Poco::DigestEngine::Digest d = engine.digest(); |
83 | return std::string(reinterpret_cast<const char*>(&d[0]), d.size()); |
84 | } |
85 | |
86 | std::string digestToHexString(Poco::DigestEngine& engine) |
87 | { |
88 | Poco::DigestEngine::Digest d = engine.digest(); |
89 | return Poco::DigestEngine::digestToHex(d); |
90 | } |
91 | |
92 | std::string digestToBase64(Poco::DigestEngine& engine) |
93 | { |
94 | return encodeBase64(digestToBinaryString(engine)); |
95 | } |
96 | |
97 | std::string hashCredentials(const std::string& username, const std::string& password) |
98 | { |
99 | Poco::MD5Engine md5; |
100 | md5.update(username); |
101 | md5.update(std::string(":mongo:" )); |
102 | md5.update(password); |
103 | return digestToHexString(md5); |
104 | } |
105 | |
106 | std::string createNonce() |
107 | { |
108 | Poco::MD5Engine md5; |
109 | Poco::RandomInputStream randomStream; |
110 | Poco::Random random; |
111 | for (int i = 0; i < 4; i++) |
112 | { |
113 | md5.update(static_cast<char>(randomStream.get())); |
114 | md5.update(random.nextChar()); |
115 | } |
116 | return digestToHexString(md5); |
117 | } |
118 | } |
119 | |
120 | |
121 | Database::Database(const std::string& db): |
122 | _dbname(db) |
123 | { |
124 | } |
125 | |
126 | |
127 | Database::~Database() |
128 | { |
129 | } |
130 | |
131 | |
132 | bool Database::authenticate(Connection& connection, const std::string& username, const std::string& password, const std::string& method) |
133 | { |
134 | if (username.empty()) throw Poco::InvalidArgumentException("empty username" ); |
135 | if (password.empty()) throw Poco::InvalidArgumentException("empty password" ); |
136 | |
137 | if (method == AUTH_MONGODB_CR) |
138 | return authCR(connection, username, password); |
139 | else if (method == AUTH_SCRAM_SHA1) |
140 | return authSCRAM(connection, username, password); |
141 | else |
142 | throw Poco::InvalidArgumentException("authentication method" , method); |
143 | } |
144 | |
145 | |
146 | bool Database::authCR(Connection& connection, const std::string& username, const std::string& password) |
147 | { |
148 | std::string nonce; |
149 | Poco::SharedPtr<QueryRequest> pCommand = createCommand(); |
150 | pCommand->selector().add<Poco::Int32>("getnonce" , 1); |
151 | |
152 | ResponseMessage response; |
153 | connection.sendRequest(*pCommand, response); |
154 | if (response.documents().size() > 0) |
155 | { |
156 | Document::Ptr pDoc = response.documents()[0]; |
157 | if (pDoc->getInteger("ok" ) != 1) return false; |
158 | nonce = pDoc->get<std::string>("nonce" , "" ); |
159 | if (nonce.empty()) throw Poco::ProtocolException("no nonce received" ); |
160 | } |
161 | else throw Poco::ProtocolException("empty response for getnonce" ); |
162 | |
163 | std::string credsDigest = hashCredentials(username, password); |
164 | |
165 | Poco::MD5Engine md5; |
166 | md5.update(nonce); |
167 | md5.update(username); |
168 | md5.update(credsDigest); |
169 | std::string key = digestToHexString(md5); |
170 | |
171 | pCommand = createCommand(); |
172 | pCommand->selector() |
173 | .add<Poco::Int32>("authenticate" , 1) |
174 | .add<std::string>("user" , username) |
175 | .add<std::string>("nonce" , nonce) |
176 | .add<std::string>("key" , key); |
177 | |
178 | connection.sendRequest(*pCommand, response); |
179 | if (response.documents().size() > 0) |
180 | { |
181 | Document::Ptr pDoc = response.documents()[0]; |
182 | return pDoc->getInteger("ok" ) == 1; |
183 | } |
184 | else throw Poco::ProtocolException("empty response for authenticate" ); |
185 | } |
186 | |
187 | |
188 | bool Database::authSCRAM(Connection& connection, const std::string& username, const std::string& password) |
189 | { |
190 | std::string clientNonce(createNonce()); |
191 | std::string clientFirstMsg = Poco::format("n=%s,r=%s" , username, clientNonce); |
192 | |
193 | Poco::SharedPtr<QueryRequest> pCommand = createCommand(); |
194 | pCommand->selector() |
195 | .add<Poco::Int32>("saslStart" , 1) |
196 | .add<std::string>("mechanism" , AUTH_SCRAM_SHA1) |
197 | .add<Binary::Ptr>("payload" , new Binary(Poco::format("n,,%s" , clientFirstMsg))) |
198 | .add<bool>("authAuthorize" , true); |
199 | |
200 | ResponseMessage response; |
201 | connection.sendRequest(*pCommand, response); |
202 | |
203 | Int32 conversationId = 0; |
204 | std::string serverFirstMsg; |
205 | |
206 | if (response.documents().size() > 0) |
207 | { |
208 | Document::Ptr pDoc = response.documents()[0]; |
209 | if (pDoc->getInteger("ok" ) == 1) |
210 | { |
211 | Binary::Ptr pPayload = pDoc->get<Binary::Ptr>("payload" ); |
212 | serverFirstMsg = pPayload->toRawString(); |
213 | conversationId = pDoc->get<Int32>("conversationId" ); |
214 | } |
215 | else return false; |
216 | } |
217 | else throw Poco::ProtocolException("empty response for saslStart" ); |
218 | |
219 | std::map<std::string, std::string> kvm = parseKeyValueList(serverFirstMsg); |
220 | const std::string serverNonce = kvm["r" ]; |
221 | const std::string salt = decodeBase64(kvm["s" ]); |
222 | const unsigned iterations = Poco::NumberParser::parseUnsigned(kvm["i" ]); |
223 | const Poco::UInt32 dkLen = 20; |
224 | |
225 | std::string hashedPassword = hashCredentials(username, password); |
226 | |
227 | Poco::PBKDF2Engine<Poco::HMACEngine<Poco::SHA1Engine> > pbkdf2(salt, iterations, dkLen); |
228 | pbkdf2.update(hashedPassword); |
229 | std::string saltedPassword = digestToBinaryString(pbkdf2); |
230 | |
231 | std::string clientFinalNoProof = Poco::format("c=biws,r=%s" , serverNonce); |
232 | std::string authMessage = Poco::format("%s,%s,%s" , clientFirstMsg, serverFirstMsg, clientFinalNoProof); |
233 | |
234 | Poco::HMACEngine<Poco::SHA1Engine> hmacKey(saltedPassword); |
235 | hmacKey.update(std::string("Client Key" )); |
236 | std::string clientKey = digestToBinaryString(hmacKey); |
237 | |
238 | Poco::SHA1Engine sha1; |
239 | sha1.update(clientKey); |
240 | std::string storedKey = digestToBinaryString(sha1); |
241 | |
242 | Poco::HMACEngine<Poco::SHA1Engine> hmacSig(storedKey); |
243 | hmacSig.update(authMessage); |
244 | std::string clientSignature = digestToBinaryString(hmacSig); |
245 | |
246 | std::string clientProof(clientKey); |
247 | for (std::size_t i = 0; i < clientProof.size(); i++) |
248 | { |
249 | clientProof[i] ^= clientSignature[i]; |
250 | } |
251 | |
252 | std::string clientFinal = Poco::format("%s,p=%s" , clientFinalNoProof, encodeBase64(clientProof)); |
253 | |
254 | pCommand = createCommand(); |
255 | pCommand->selector() |
256 | .add<Poco::Int32>("saslContinue" , 1) |
257 | .add<Poco::Int32>("conversationId" , conversationId) |
258 | .add<Binary::Ptr>("payload" , new Binary(clientFinal)); |
259 | |
260 | std::string serverSecondMsg; |
261 | connection.sendRequest(*pCommand, response); |
262 | if (response.documents().size() > 0) |
263 | { |
264 | Document::Ptr pDoc = response.documents()[0]; |
265 | if (pDoc->getInteger("ok" ) == 1) |
266 | { |
267 | Binary::Ptr pPayload = pDoc->get<Binary::Ptr>("payload" ); |
268 | serverSecondMsg = pPayload->toRawString(); |
269 | } |
270 | else return false; |
271 | } |
272 | else throw Poco::ProtocolException("empty response for saslContinue" ); |
273 | |
274 | Poco::HMACEngine<Poco::SHA1Engine> hmacSKey(saltedPassword); |
275 | hmacSKey.update(std::string("Server Key" )); |
276 | std::string serverKey = digestToBinaryString(hmacSKey); |
277 | |
278 | Poco::HMACEngine<Poco::SHA1Engine> hmacSSig(serverKey); |
279 | hmacSSig.update(authMessage); |
280 | std::string serverSignature = digestToBase64(hmacSSig); |
281 | |
282 | kvm = parseKeyValueList(serverSecondMsg); |
283 | std::string serverSignatureReceived = kvm["v" ]; |
284 | |
285 | if (serverSignature != serverSignatureReceived) |
286 | throw Poco::ProtocolException("server signature verification failed" ); |
287 | |
288 | pCommand = createCommand(); |
289 | pCommand->selector() |
290 | .add<Poco::Int32>("saslContinue" , 1) |
291 | .add<Poco::Int32>("conversationId" , conversationId) |
292 | .add<Binary::Ptr>("payload" , new Binary); |
293 | |
294 | connection.sendRequest(*pCommand, response); |
295 | if (response.documents().size() > 0) |
296 | { |
297 | Document::Ptr pDoc = response.documents()[0]; |
298 | return pDoc->getInteger("ok" ) == 1; |
299 | } |
300 | else throw Poco::ProtocolException("empty response for saslContinue" ); |
301 | } |
302 | |
303 | |
304 | Int64 Database::count(Connection& connection, const std::string& collectionName) const |
305 | { |
306 | Poco::SharedPtr<Poco::MongoDB::QueryRequest> countRequest = createCountRequest(collectionName); |
307 | |
308 | Poco::MongoDB::ResponseMessage response; |
309 | connection.sendRequest(*countRequest, response); |
310 | |
311 | if (response.documents().size() > 0) |
312 | { |
313 | Poco::MongoDB::Document::Ptr doc = response.documents()[0]; |
314 | return doc->getInteger("n" ); |
315 | } |
316 | |
317 | return -1; |
318 | } |
319 | |
320 | |
321 | Poco::MongoDB::Document::Ptr Database::ensureIndex(Connection& connection, const std::string& collection, const std::string& indexName, Poco::MongoDB::Document::Ptr keys, bool unique, bool background, int version, int ttl) |
322 | { |
323 | Poco::MongoDB::Document::Ptr index = new Poco::MongoDB::Document(); |
324 | index->add("ns" , _dbname + "." + collection); |
325 | index->add("name" , indexName); |
326 | index->add("key" , keys); |
327 | |
328 | if (version > 0) |
329 | { |
330 | index->add("version" , version); |
331 | } |
332 | |
333 | if (unique) |
334 | { |
335 | index->add("unique" , true); |
336 | } |
337 | |
338 | if (background) |
339 | { |
340 | index->add("background" , true); |
341 | } |
342 | |
343 | if (ttl > 0) |
344 | { |
345 | index->add("expireAfterSeconds" , ttl); |
346 | } |
347 | |
348 | Poco::SharedPtr<Poco::MongoDB::InsertRequest> insertRequest = createInsertRequest("system.indexes" ); |
349 | insertRequest->documents().push_back(index); |
350 | connection.sendRequest(*insertRequest); |
351 | |
352 | return getLastErrorDoc(connection); |
353 | } |
354 | |
355 | |
356 | Document::Ptr Database::getLastErrorDoc(Connection& connection) const |
357 | { |
358 | Document::Ptr errorDoc; |
359 | |
360 | Poco::SharedPtr<Poco::MongoDB::QueryRequest> request = createQueryRequest("$cmd" ); |
361 | request->setNumberToReturn(1); |
362 | request->selector().add("getLastError" , 1); |
363 | |
364 | Poco::MongoDB::ResponseMessage response; |
365 | connection.sendRequest(*request, response); |
366 | |
367 | if (response.documents().size() > 0) |
368 | { |
369 | errorDoc = response.documents()[0]; |
370 | } |
371 | |
372 | return errorDoc; |
373 | } |
374 | |
375 | |
376 | std::string Database::getLastError(Connection& connection) const |
377 | { |
378 | Document::Ptr errorDoc = getLastErrorDoc(connection); |
379 | if (!errorDoc.isNull() && errorDoc->isType<std::string>("err" )) |
380 | { |
381 | return errorDoc->get<std::string>("err" ); |
382 | } |
383 | |
384 | return "" ; |
385 | } |
386 | |
387 | |
388 | Poco::SharedPtr<Poco::MongoDB::QueryRequest> Database::createCountRequest(const std::string& collectionName) const |
389 | { |
390 | Poco::SharedPtr<Poco::MongoDB::QueryRequest> request = createQueryRequest("$cmd" ); |
391 | request->setNumberToReturn(1); |
392 | request->selector().add("count" , collectionName); |
393 | return request; |
394 | } |
395 | |
396 | |
397 | } } // namespace Poco::MongoDB |
398 | |