Solved What algorhythm is used for encrypting user passwords?


  • Since I want to use NodeBB database for authenticating users on the other site, I need to know what is the algorhythm. Thanks!

  • NodeBB

    I tested this and Im getting the expected result.

    router.get('/test', async (req, res) => {
    	const crypto = require('crypto');
    	const bcrypt = require('bcryptjs');
    
    	const rounds = 12;
    	const mypassword = '123456';
    	const shaPassword = crypto.createHash('sha512').update(mypassword).digest('hex');
    	const salt = await bcrypt.genSalt(parseInt(rounds, 10));
    	const hashedPassword = await bcrypt.hash(shaPassword, salt);
    	console.log('hashedPassword', hashedPassword);
    
    	// testing
    	const mypasswordtry = '123456'; 
    	const shaPasswordtry = crypto.createHash('sha512').update(mypasswordtry).digest('hex');
    
    	res.json({
    		'should be true': bcrypt.compareSync(shaPasswordtry, hashedPassword),
    		'should be false': bcrypt.compareSync('asdasdasa', hashedPassword),
    	});
    });
    

    Prints out

    {
        "should be true": true,
        "should be false": false
    }
    
  • NodeBB

    We use bcrypt


  • Are you sure it's that simple?

    I'm importing users from .csv file over Write API and the passwords that are listed in .csv are getting encrypted and written into password property of User object, but when I encrypt the same password manually and compare with what is saved in database, it's not the same.

    I think something more is happening there besides bcrypt. I used this site to check: https://bcrypt-generator.com/. Tried up to 17 rounds.


  • Seems like I'm not getting how it all works here. First of all, bcrypt generates hashes according to salt, but where that salt is set? And how then someone is actually checked against database on login, client must know what is salt also, right?

    I need to manually replicate login system of NodeBB on another site. I just want to use user database of NodeBB on some other site.


  • Tried this also:

    Online Bcrypt Hashed Matcher can't match passwords no way.

  • GNU/Linux Admin

    @pyc4 The file you should be looking at is src/passwords.js

    Specifically, the plaintext input is hashed using sha512, and then a salt is generated alongside. The password is then hashed using bcrypt before being stored into the database.

    The sha wrapping allows usage of passwords longer than 73 characters in length. Prior to this, the input was truncated down to 73 characters due to a limitation in bcrypt.


  • @julian

    But if I hash password using SHA512 (https://passwordsgenerator.net/sha512-hash-generator/), I get 128 characters, how then bcrypt hashes 128 chars when 73 is the limit?

    Sorry this must sound like some crappy question 🙂 But definitely I just can't wrap my head around it now, seems it's so simple but yet complicated.

    Anyways, is salt somehow important when making checks if passwords match against database or not? In other words, how would I know what (random?) salt is generated when hash is made for the first time and written to database?

    I see the number of rounds for bcrypt is custom, but where and how it is set?

  • Community Rep

    @pyc4 My crypto is rusty. That said, if I understand correctly, the sha512 hash, regardless of initial input, results in 64 characters (/ 512 8), which then ducks under bcrypts 73 char limit?

    Hope this helps. Have fun down the crypto rabbit hole. 😈 🐕


  • @gotwf

    So I guess if I get something like this from online SHA512 generator: 4ffcc3cce9a56025aa0126c2fe7ec247e8a6e5ee6fdd5e854bb3761b66e5c5c4909189ad054e44a473cc29fd461fccf1965aebe5b17dfbaac9b994112c5a33a3

    It looks like it can be converted to 64 (ascii?) characters...

  • Community Rep

    @pyc4 Be advised that it has been many years since I tried wrapping my noggin' around this stuff.... How it gets from 512 bits to 64 bytes for bcrypt input I do not know.

    But maybe this helps. Seems to me @julian and crew may have opted for something akin to "Solution 3"?

    Password hash truncation

    The bcrypt algorithm involves repeatedly encrypting the 24-byte text:

    OrpheanBeholderScryDoubt (24-bytes)

    This generates 24 bytes of ciphertext, e.g.:

    85 20 af 9f 03 3d b3 8c 08 5f d2 5e 2d aa 5e 84 a2 b9 61 d2 f1 29 c9 a4 (24-bytes)

    The canonical OpenBSD implementation truncates this to 23 bytes:

    85 20 af 9f 03 3d b3 8c 08 5f d2 5e 2d aa 5e 84 a2 b9 61 d2 f1 29 c9 (23-bytes)

    It is unclear why the canonical implementation deletes 8-bits from the resulting password hash.

    These 23 bytes become 31 characters when radix-64 encoded:

    fQAtluK7q2uGV7HcJYncfII3WbJvIai (31-characters)


  • @gotwf Well I don't exactly need to know the depths of such algorithms, what I need is just a little bit of information on how to successfully use functions that are involved here... think password.js have some crucial information, I see what libraries are used and what is most important, think that would be sufficient but I guess I'll have few more questions here most probably.


  • Just a little bit of help needed here:

    src/user/password.js:           return await Password.hash(nconf.get('bcrypt_rounds') || 12, password);
    

    Means that if bcrypt_rounds is not found in nconf, 12 rounds is used?

    Any help on where nconf configuration is stored?

  • Community Rep

    @pyc4 As mentioned previously hereabouts on community, I do not code nodebb but somewhere in the recesses of my mind I seem to recall that the default number of rounds for bcrypt itself is seven. Since I am so rusty w/that stuff, I decided to duckduckgo it and ran across the node.bcrypt.js bcrypt API:

    API

    BCrypt.

    genSaltSync(rounds, minor)
    rounds - [OPTIONAL] - the cost of processing the data. (default - 10)
    minor - [OPTIONAL] - minor version of bcrypt to use. (default - b)
    genSalt(rounds, minor, cb)
    rounds - [OPTIONAL] - the cost of processing the data. (default - 10)
    minor - [OPTIONAL] - minor version of bcrypt to use. (default - b)
    cb - [OPTIONAL] - a callback to be fired once the salt has been generated. uses eio making it asynchronous. If cb is not specified, a Promise is returned if Promise support is available.
    err - First parameter to the callback detailing any errors.
    salt - Second parameter to the callback providing the generated salt.

    Which seems to indicate a default salt rounds of ten.

    Maybe grep that code/file for "genSalt" could provide additional insights? Hope this helps get you sorted. Else you'll have to wait/hope for one of the code savvy dev helpful helper types to weigh in because this is about all I have got on the subject.

    Good luck! 🐕

  • Global Moderator Plugin & Theme Dev

    @pyc4

    Means that if bcrypt_rounds is not found in nconf, 12 rounds is used?

    Yes

    Any help on where nconf configuration is stored?

    config.json but generally it's not specified so our default of 12 is used.

  • GNU/Linux Admin

    @PitaJ is correct, bcrypt_rounds can be set anywhere that nconf reads configs from, so mainly config.json but also via env var, or passed into the nodebb executable as an argument.

    But it's optional, so it usually just falls back to the default of 12 rounds. As computation power increases, we increase this number to compensate. A login attempt should block for some noticible amount of time (perhaps a second or two).

    @pyc4 said:

    Well I don't exactly need to know the depths of such algorithms,

    Correct, and neither do we. We implicitly trust in the security of the bcrypt library because rolling our own crypto is almost always a bad idea 😅

    That said the sha-wrapping is outside of the bcrypt lib, so I honestly couldn't say whether there are bugs in that implementation.


  • @julian @Pitaj @gotwf

    Thanks for your replies, it was useful and I'm making some progress here. Please stay with me a bit more so I can finish this, help's appreciated here very much 🙂

    Right now I'm struggling with SHA512 hashing, here's the code that I took from password.js to do hashing:

        passwordSHA512 = crypto.createHash('sha512').update(passwordSHA512).digest('hex');
        console.log(passwordSHA512);    // 4ffcc3cce9a56025aa0126c2fe7ec247e8a6e5ee6fdd5e854bb3761b66e5c5c4909189ad054e44a473cc29fd461fccf1965aebe5b17dfbaac9b994112c5a33a3
        const salt = bcrypt.genSaltSync(12);
        const hash = bcrypt.hashSync(passwordSHA512, salt);
        console.log(salt); // $2a$12$ZDFzbDOVrZSZ.L.RFWQiqe
        console.log(hash); // $2a$12$ZDFzbDOVrZSZ.L.RFWQiqej/ed5M6b/C06Hz/mRCuOd1fMVpzVF/.
    

    So it's used in completely the same way as in password.js, but what I finally get is some other hash string, which is not the same as hash string generated by NodeBB which is saved in its database.

    As I've previously said, the length of hash I get after SHA512 is applied looks suspicious to me, and it's 128 chars long hex value which I don't know how it's supposed to be supplied to bcrypt because of 72 chars limit. Probably that HEX value is somehow converted to shorter string? But if it's somehow automatic, it's supposed to work, but what I get is completely some other value than I need to get.

  • Global Moderator Plugin & Theme Dev

    @pyc4 if you have older user passwords, it's likely those were hashed without the sha step


  • @PitaJ I'm playing with NodeBB from version 1.19.1, so I guess it was always that SHA512 was enabled? ... I imported users through Write API, and within every user stored object I have password:shaWrapped property set to 1.

  • GNU/Linux Admin

    @pyc4 if the salt is different between NodeBB and your app, the resulting hash will of course be different.

    The salt is randomly generated for each password, so it can't be shared.

    But the salt itself is prepended to the hash, so the real test is whether running .compare() on your app shows that the password matches.


  • @julian

    OK, just let me be clear on the step really I'm not fully sure it's ok?

    1. user-entered password is put through
      crypto.createHash('sha512').update(password).digest('hex');

    2. then I use that as the first argument for bcrypt.compare() function, and the second argument is password fetched from NodeBB database, and if true is returned entered password is matched against stored password.

    Still I don't understand how is it possible to compare if I don't have original salt, but if it could work like how I described it, it's good enough for me.


Suggested Topics

| |