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
33namespace Poco {
34namespace MongoDB {
35
36
37const std::string Database::AUTH_MONGODB_CR("MONGODB-CR");
38const std::string Database::AUTH_SCRAM_SHA1("SCRAM-SHA-1");
39
40
41namespace
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
121Database::Database(const std::string& db):
122 _dbname(db)
123{
124}
125
126
127Database::~Database()
128{
129}
130
131
132bool 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
146bool 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
188bool 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
304Int64 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
321Poco::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
356Document::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
376std::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
388Poco::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