简体   繁体   中英

c++ unordered_map string key not always found when it exists? string v char?

I'm having trouble with a unit test I've written. Basically I'm storing users name + salt + a hash of a key derived from a password in an unordered_map. The name is the key. The salt + hash are held in a struct with 16 and 32 bytes respectively. This unordered_map is created from a stream. In my test the stream is a stringstream, but in my build I will be using a fstream.

So essentially my code is trying to decide whether to create a new user. If it doesnt find the users name in the stream it will insert it. If it does find the users name in the stream it doesnt insert it.

My unit test should always find the users name because I have hardcoded it to do so. However it doesn't always find it and sometimes it fails. Specifically where I expect users.size() to equal 1, it sometimes equals 2, implying the user was added despite the key existing there. (I've printed the keys of users and they are the same...

What am I doing wrong?

Also I'm pretty new to C++ so if you can point out anything I'm doing wrong or have any recommendations please suggest them. Thanks!

Edited: displaying code without modifications

my_password_manager.h

#define SALT_LEN 16
#define DIGEST_LEN 32 //we use sha 256 for digest
#define ITER 100000

struct SaltDigest
{
  unsigned char salt[SALT_LEN];
  unsigned char digest[DIGEST_LEN];
};

class MyPasswordManager
{
private:
  unordered_map<string, AppDetails> apps;
  void AddUsersFromStream(iostream &mystream);
  void CreateDigestFromPasswordAndSalt(string p, unsigned char *salt, unsigned char *digest);

public:
  // username, salt, digest
  unordered_map<string, SaltDigest> users;
  string current_user;
  // constructor loads the list of user profiles from file
  MyPasswordManager() {}

  // Creates a profile
  bool CreateProfile(string user, string password, iostream &mystream);
};

my_password_manager.cc

void MyPasswordManager::AddUsersFromStream(iostream &mystream)
{
    if (mystream.good())
    {
        while (mystream.peek() != EOF)
        {
            // read first byte for # of chars for username
            char *userlen = new char[sizeof(int)];
            mystream.read(userlen, sizeof(int));
            char *username = new char[*userlen];
            mystream.read(username, *userlen);

            unsigned char *salt = new unsigned char[SALT_LEN];
            mystream.read((char *)salt, SALT_LEN);

            unsigned char *digest = new unsigned char[DIGEST_LEN];
            mystream.read((char *)digest, DIGEST_LEN);
            SaltDigest sd;
            memcpy(sd.salt, salt, SALT_LEN);
            memcpy(sd.digest, digest, DIGEST_LEN);

            pair<string, SaltDigest> user_details(username, sd);
            users.insert(user_details);
            delete[] username;
            delete[] userlen;
            delete[] salt;
            delete[] digest;
        }
    }
}

void MyPasswordManager::CreateDigestFromPasswordAndSalt(string p, unsigned char *salt, unsigned char *digest)
{
    // create a key from the password & salt
    char *pass = (char *)p.c_str();
    int passlen = p.size();
    unsigned int keylen = 16;
    unsigned char *key = new unsigned char[keylen];
    if (1 != PKCS5_PBKDF2_HMAC_SHA1(pass, passlen, salt, SALT_LEN, ITER, keylen, key))
        handleErrors();

    // hash key
    digest_message(key, keylen, &digest);
    delete[] key;
}

// Creat a user profile
bool MyPasswordManager::CreateProfile(string u, string p, iostream &mystream)
{
    if (users.size() == 0)
        AddUsersFromStream(mystream);

    if (users.find(u) != users.end())
    {
        return false;
    }

    if (!mystream.good())
    {
        mystream.clear();
    }
    mystream.seekp(0, ios::end);

    // create a random salt
    unsigned char *salt = new unsigned char[SALT_LEN];
    if (1 != RAND_bytes(salt, SALT_LEN))
        handleErrors();

    unsigned char *digest = new unsigned char[DIGEST_LEN];
    CreateDigestFromPasswordAndSalt(p, salt, digest);

    // write # of chars for username, the username, the salt & key
    int userlen = u.size();
    mystream.write(reinterpret_cast<const char *>(&userlen), sizeof(int));
    mystream.write(u.c_str(), userlen);
    mystream.write((char *)salt, SALT_LEN);
    mystream.write((char *)digest, DIGEST_LEN);

    // insert into users object
    SaltDigest sd;
    memcpy(sd.salt, salt, SALT_LEN);
    memcpy(sd.digest, digest, DIGEST_LEN);

    pair<string, SaltDigest> user_details(u, sd);
    users.insert(user_details);
    current_user = u;

    delete[] digest;
    delete[] salt;
    return true;
}

encryption.cc which has the digest_message function. (This is taken almost directly from openssl)

void digest_message(const unsigned char *message, size_t message_len, unsigned char **digest)
{
    EVP_MD_CTX *mdctx;
    // Create a Message Digest context
    if ((mdctx = EVP_MD_CTX_create()) == NULL)
        handleErrors();
    // Initialise the context by identifying the algorithm to be used
    if (1 != EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL))
        handleErrors();

    // Provide the message whose digest needs to be calculated. Messages can be divided into sections and provided over a number of calls to the library if necessary
    if (1 != EVP_DigestUpdate(mdctx, message, message_len))
        handleErrors();

    // Caclulate the digest
    if ((*digest = (unsigned char *)OPENSSL_malloc(EVP_MD_size(EVP_sha256()))) == NULL)
        handleErrors();

    unsigned int *digest_len = nullptr;
    if (1 != EVP_DigestFinal_ex(mdctx, *digest, digest_len))
        handleErrors();
    // Clean up the context if no longer required
    EVP_MD_CTX_destroy(mdctx);
}

And finally my test which uses google_test

namespace
{

class CreateProfileTest : public testing::Test
{
  public:
    MyPasswordManagerTest() : existinguser_("ExistingUser1"),
                              existingpassword_("somepassword"),
                              digestlen_(32),
                              saltlen_(16) {}

  protected:
    MyPasswordManager mpm;
    stringstream emptystream_;
    stringstream existingstream_;
    string existinguser_;
    string existingpassword_;
    unsigned char *existingsalt_;
    unsigned char *digest_;
    unsigned int digestlen_;
    int saltlen_;

    virtual void SetUp()
    {
        int keylen = 16;
        int iter = 100000;
        digest_ = new unsigned char[digestlen_];
        existingsalt_ = new unsigned char[saltlen_];
        unsigned char *key = new unsigned char[keylen];
        unsigned int userlen = existinguser_.size();
        char *pass = (char *)existingpassword_.c_str();
        int passlen = existingpassword_.size();

        if (1 != RAND_bytes(existingsalt_, saltlen_))
            handleErrors();

        if (1 != PKCS5_PBKDF2_HMAC_SHA1(pass, passlen, existingsalt_, saltlen_, iter, keylen, key))
            handleErrors();

        //hash the key
        digest_message(key, keylen, &digest_);
        cout << userlen << endl;
        existingstream_.write(reinterpret_cast<const char *>(&userlen), sizeof(int));
        existingstream_.write(existinguser_.c_str(), userlen);
        existingstream_.write((char *)existingsalt_, saltlen_);
        existingstream_.write((char *)digest_, digestlen_);
        delete[] key;
    }

    virtual void TearDown()
    {
        delete[] existingsalt_;
        delete[] digest_;
    }
};

TEST_F(CreateProfileTest, DoesntAddUsersWhenTheyExistInStream)
{
    EXPECT_FALSE(mpm.CreateProfile("ExistingUser1", "MasterPassword", existingstream_));
    EXPECT_EQ(1, mpm.users.size());
    EXPECT_NE(mpm.users.find("ExistingUser1"), mpm.users.end());
}

When you encode the username into stream, you take the length of the word but pass .c_str()

existingstream_.write(reinterpret_cast<const char *>(&userlen), sizeof(int));
existingstream_.write(existinguser_.c_str(), userlen);

.c_str() adds terminating '\\0' , which you cut off by passing pure word length. And that is fine as long as you know how long is the word and pass that data along.

When you assemble it on the other end, you need to use string constructor that takes length param and pass your word length to it. Default constructor will look for terminating '\\0' and sometimes there might even be one, which explains why it worked for you sometimes. So to make sure the string is constructed correctly, you need to tell the constructor how long is the username, like this:

 pair<string, SaltDigest> user_details(std::string(username, (int)*userlen), sd);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM