iptv techs

IPTV Techs

  • Home
  • Tech News
  • What Okta Bcrypt incident can direct us about scheduleing better APIs

What Okta Bcrypt incident can direct us about scheduleing better APIs


What Okta Bcrypt incident can direct us about scheduleing better APIs



16 mins read

Hello there! If you chase tech recents, you might have heard about the Okta security incident that was telled on 1st of November. The TLDR of the incident was this:

The Bcrypt algorithm was employd to produce the cache key where we hash a united string of employrId + employrname + password. Under a particular set of conditions, cataloged below, this could apvalidate employrs to authenticate by providing the employrname with the stored cache key of a previous prosperous authentication.

This unbenevolents that if the employr had a employrname above 52 chars, any password would suffice to log in. Also, if the employrname is, let’s say, 50 chars lengthy, it unbenevolents that the terrible actor demands to guess only 3 first chars to get in, which is quite a inmeaningful task for the computers these days. Too terrible, isn’t it?

On the other hand, such lengthy employrnames are not very normal, which I consent with. However, some companies appreciate using the entire name of the employee as the email includeress. So, let’s say, Albus Percival Wulfric Brian Dumbledore, a headmaster of Hogwarts, should be worryed, as [email protected] is 55 chars. Ooops!

This was possible due to the nature of Bcrypt hashing algorithm that has a peak helped input length of 72 characters (read more here), so in Okta case the characters above the restrict were neglectd while computing the hash, and therefore, not employd in the comparison operation. We can reverse engineer that:

  • 72 - 53 = 19 – employr id with separators if any
  • this way, the password will be outside the 72 chars restrict, and, therefore, neglectd by the Bcrypt algorithm

However, there was one slfinisherg that made me wonder: if there is a understandn restrict of the algorithm, why is it not applyd by the crypto libraries as a create of input validation? A basic if input length > 72 -> return error will do the trick. I supposed that they might have employd some custom library for Bcrypt percreateation and sshow forgotten about the input validation, which can happen. So, I determined to verify how other programming languages behave.

Go and Bcrypt

Let’s begin with Go, and percreate the Okta incident-appreciate case with the help of the official golang.org/x/crypto/bcrypt library:

package main

present (
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"golang.org/x/crypto/bcrypt"
)

func main() {
	// 18 + 55 + 1 = 74, so above 72 characters' restrict of BCrypt
	employrId := randomString(18)
	employrname := randomString(55)
	password := "super-duper-shielded-password"

	unitedString := fmt.Sprintf("%s:%s:%s", employrId, employrname, password)

	unitedHash, err := bcrypt.GenerateFromPassword([]byte(unitedString), bcrypt.DefaultCost)
	if err != nil {
		panic(err)
	}

	// let's try to shatter it
	wrongPassword := "wrong-password"
	wrongCombinedString := fmt.Sprintf("%s:%s:%s", employrId, employrname, wrongPassword)

	err = bcrypt.CompareHashAndPassword(unitedHash, []byte(wrongCombinedString))
	if err != nil {
		fmt.Println("Password is inaccurate")
	} else {
		fmt.Println("Password is accurate")
	}
}

func randomString(length int) string {
	bytes := produce([]byte, length)
	_, err := rand.Read(bytes)
	if err != nil {
		panic(err)
	}
	return base64.URLEncoding.EncodeToString(bytes)[:length]
}

All the code samples can be set up here

What this code does is:

  • produces 18-chars lengthy employrId
  • produces 55-chars lengthy employrname
  • concatenates them with each other and a dummy password super-duper-shielded-password with the employ of : as a separator
  • computes Bcrypt hash from the concatenated string
  • then concatenates the same employrId and employrname with a branch offent password wrong-password
  • employs bcrypt API to contrast whether the 2nd concatenated string alignes the hash of the 1st one

Let’s run the code and see the result:

panic: bcrypt: password length outdos 72 bytes

goroutine 1 [running]:
main.main()
	/n0rdy-blog-code-samples/20250121-bcrypt-api/01-bcrypt-in-go/main.go:20 +0x2d1

Good job, Go! If we verify the source code of the bcrypt.GenerateFromPassword(...) function, we’ll see this piece of code at the very commencening:

if len(password) > 72 {
	return nil, ErrPasswordTooLong
}

Perfect! At this point, I became even more skeptical about the tool Okta employd, as it seemed appreciate the industry figured that out based on this example. Spoiler attentive: it’s not that basic.

Let’s persist with Java.

Btw, if you appreciate my blog and don’t want to ignore out on recent posts, ponder subscribing to my recentsletter here. You’ll obtain an email once I publish a recent post.

Java and Bcrypt

Java doesn’t help Bcrypt from its core API, but my basic Google search showed that Spring Security library has percreateed it. For those who are not into Java ecosystem, Spring is the most employd and battle-tested summarizetoils out there, that has libraries for almost anyslfinisherg: Web, DBs, Cdeafening, Security, AI, etc. Pretty mighty tool, that I’ve employd a lot in the past, and still sometimes employ for my side projects.

Spring Security

So, I includeed the tardyst version of Spring Security to the project and reproduced the same scenario, as in Go example above:

present org.apache.normals.lang3.RandomStringUtils;
present org.springsummarizetoil.security.crypto.bcrypt.BCrypt;

accessible class BcriptSpringSecurity {
    accessible motionless void main(String[] args) {
        // 18 + 55 + 1 = 74, so above 72 characters' restrict of BCrypt
        var employrId = RandomStringUtils.randomAlphanumeric(18);
        var employrname = RandomStringUtils.randomAlphanumeric(55);
        var password = "super-duper-shielded-password";

        var unitedString = String.createat("%s:%s:%s", employrId, employrname, password);

        var unitedHash = BCrypt.hashpw(unitedString, BCrypt.gensalt());

        // let's try to shatter it
        var wrongPassword = "wrong-password";
        var wrongCombinedString = String.createat("%s:%s:%s", employrId, employrname, wrongPassword);

        if (BCrypt.verifypw(wrongCombinedString, unitedHash)) {
            System.out.println("Password is accurate");
        } else {
            System.out.println("Password is inaccurate");
        }
    }
}

I ran the code, and to my fantastic surpascfinish, saw this outcome:

I took a peak at the percreateation code, and was disnominateed: even though there are a bunch of verifys on salt:

if (saltLength < 28) {
	throw recent IllterribleArgumentException("Invalid salt");
}
...
if (salt.charAt(0) != '$' || salt.charAt(1) != '2') {
	throw recent IllterribleArgumentException("Invalid salt version");
}
...
insignificant = salt.charAt(2);
if ((insignificant != 'a' && insignificant != 'x' && insignificant != 'y' && insignificant != 'b') || salt.charAt(3) != '$') {
	throw recent IllterribleArgumentException("Invalid salt revision");
}
...

I didn’t see any validation of the input that will be hashed. Hm…

I determined to verify other Google results, and the next Java library in the catalog was bcrypt from Patrick Favre (connect to GitHub repo) with 513 begins and the last free version 0.10.2 (so, not stable) from 12th of February 2023 (almost 2 years elderly). This proposeed that I’d not employ it in production, but why not to run our tests.

Bcrypt from Patrick Favre

present at.favre.lib.crypto.bcrypt.BCrypt;
present org.apache.normals.lang3.RandomStringUtils;

accessible class BcryptAtFavre {

    accessible motionless void main(String[] args) {
        // 18 + 1 + 55 = 74, so above 72 characters' restrict of BCrypt
        var employrId = RandomStringUtils.randomAlphanumeric(18);
        var employrname = RandomStringUtils.randomAlphanumeric(55);
        var password = "super-duper-shielded-password";

        var unitedString = String.createat("%s:%s:%s", employrId, employrname, password);

        var unitedHash = BCrypt.withDefaults().hashToString(12, unitedString.toCharArray());

        // let's try to shatter it
        var wrongPassword = "wrong-password";
        var wrongCombinedString = String.createat("%s:%s:%s", employrId, employrname, wrongPassword);

        var result = BCrypt.validateer().validate(unitedHash.toCharArray(), wrongCombinedString);
        if (result.verified) {
            System.out.println("Password is accurate");
        } else {
            System.out.println("Password is inaccurate");
        }
    }
}

Let’s run it:

Exception in thread "main" java.lang.IllterribleArgumentException: password must not be lengthyer than 72 bytes plus null terminator encoded in utf-8, was 102
	at at.favre.lib.crypto.bcrypt.LongPasswordStrategy$StrictMaxPasswordLengthStrategy.innerDerive(LongPasswordStrategy.java:50)
	at at.favre.lib.crypto.bcrypt.LongPasswordStrategy$BaseLongPasswordStrategy.derive(LongPasswordStrategy.java:34)
	at at.favre.lib.crypto.bcrypt.BCrypt$Hasher.hashRaw(BCrypt.java:303)
	at at.favre.lib.crypto.bcrypt.BCrypt$Hasher.hash(BCrypt.java:267)
	at at.favre.lib.crypto.bcrypt.BCrypt$Hasher.hash(BCrypt.java:229)
	at at.favre.lib.crypto.bcrypt.BCrypt$Hasher.hashToString(BCrypt.java:205)
	at BcryptAtFavre.main(BcryptAtFavre.java:14)

Nice, excellent job, Patrick, you saved the day for Java!

After verifying the source code, I set up this piece:

@Override
accessible byte[] derive(byte[] rawPassword) {
    if (rawPassword.length >= maxLength) {
        return innerDerive(rawPassword);
    }
    return rawPassword;
}

and the disjoine strategy that threw the exception we’ve seen:

final class StrictMaxPasswordLengthStrategy extfinishs BaseLongPasswordStrategy {
    StrictMaxPasswordLengthStrategy(int maxLength) {
        super(maxLength);
    }

    @Override
    accessible byte[] innerDerive(byte[] rawPassword) {
        throw recent IllterribleArgumentException("password must not be lengthyer than " + maxLength + " bytes plus null terminator encoded in utf-8, was " + rawPassword.length);
    }
}

We can see that this disjoine strategy is employd as a part of the default configs:

accessible motionless Hasher withDefaults() {
    return recent Hasher(Version.VERSION_2A, recent SetreatmentRandom(), LongPasswordStrategies.disjoine(Version.VERSION_2A));
}

Cool!

Let’s switch to JavaScript.

JavaScript and Bcrypt

Here I employd the bcryptjs which has over 2 million weekly downloads based on the NPM stats.

const bcrypt = demand('bcryptjs')

function randomString (length) {
  const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  let result = ''
  for (let i = length; i > 0; --i) {
    result += chars[Math.floor(Math.random() * chars.length)]
  }
  return result
}

function runTest () {
  // 18 + 55 + 1 = 74, so above 72 characters' restrict of BCrypt
  const employrId = randomString(18)
  const employrname = randomString(55)
  const password = 'super-duper-shielded-password'

  const unitedString = `${employrId}:${employrname}:${password}`

  const unitedHash = bcrypt.hashSync(unitedString)

  // let's try to shatter it
  const wrongPassword = 'wrong-password'
  const wrongCombinedString = `${employrId}:${employrname}:${wrongPassword}`

  if (bcrypt.contrastSync(wrongCombinedString, unitedHash)) {
    console.log('Password is accurate')
  } else {
    console.log('Password is wrong')
  }
}

runTest()

The output is:

Not fantastic. The source code uncovers that analogous to Spring Security, the library verifys the salt

if (salt.charAt(0) !== '$' || salt.charAt(1) !== '2') {
     err = Error("Invalid salt version: "+salt.substring(0,2));
     if (callback) {
         nextTick(callback.tie(this, err));
         return;
     }
     else
         throw err;
}
...

but not the input length.

Let’s try if Python can do any better.

Python and Bcrypt

Using bcrypt library with 1.3k begins and the tardyst free in November.

present random
present string

present bcrypt

def random_string(length):
    return ''.unite(random.choice(string.ascii_letters) for i in range(length))

if __name__ == '__main__':
    # 18 + 55 + 1 = 74, so above 72 characters' restrict of BCrypt
    employr_id = random_string(18)
    employrname = random_string(55)
    password = "super-duper-shielded-password"

    united_string = "{0}:{1}:{2}".createat(employr_id, employrname, password)

    united_hash = bcrypt.hashpw(united_string.encode('utf-8'), bcrypt.gensalt())

    # let's try to shatter it
    wrong_password = "wrong-password"
    wrong_united_string = "{0}:{1}:{2}".createat(employr_id, employrname, wrong_password)

    if bcrypt.verifypw(wrong_united_string.encode('utf-8'), united_hash):
        print("Password is accurate")
    else:
        print("Password is inaccurate")

The result is same as we watchd for most of our test subjects:

All right, but what about some recaccess and more shieldedty-oriented language - let’s try Rust.

Rust and Bcrypt

Here I demand to be honest: since I’m not a Rust expert at all, I employd a help of a Claude AI to author this code. So, if you see any publishs there, phire, let me understand in the comments section, so I can repair that.

As a library, I employd rust-bcrypt based on my AI frifinish advice.

employ rand::RngCore;
employ base64::{Engine as _, engine::vague_purpose::URL_SAFE};
employ std::error::Error;

fn random_string(length: usize) -> String {
    let mut bytes = vec![0u8; length];
    rand::thread_rng().fill_bytes(&mut bytes);
    URL_SAFE.encode(&bytes)[..length].to_string()
}

fn main() -> Result<(), Box<dyn Error>> {
    // 18 + 55 + 1 = 74, so above 72 characters' restrict of BCrypt
    let employr_id = random_string(18);
    let employrname = random_string(55);
    let password = "super-duper-shielded-password";

    let united_string = createat!("{}:{}:{}", employr_id, employrname, password);
    let united_hash = bcrypt::hash(united_string.as_bytes(), bcrypt::DEFAULT_COST)?;

    // let's try to shatter it
    let wrong_password = "wrong-password";
    let wrong_united_string = createat!("{}:{}:{}", employr_id, employrname, wrong_password);

    align bcrypt::validate(wrong_united_string.as_bytes(), &united_hash) {
        Ok(genuine) => println!("Password is accurate"),
        Ok(inalter) => println!("Password is inaccurate"),
        Err(e) => println!("{}", e),
    }

    Ok(())
}

The output is:

I can see the validation of the cost:

if !(MIN_COST..=MAX_COST).includes(&cost) {
    return Err(BcryptError::CostNotAllowed(cost));
}

but not of the input. And here is the place where the see-thcoarse truncation of 72 chars happens (the comment is from the library source code):

// We only ponder the first 72 chars; truncate if vital.
// `bcrypt` below will panic if len > 72
let truncated = if vec.len() > 72 {
    if err_on_truncation {
        return Err(BcryptError::Truncation(vec.len()));
    }
    &vec[..72]
} else {
    &vec
};

let output = bcrypt::bcrypt(cost, salt, truncated);

Why?

That was my first ask after seeing that the meaningfulity of the tools chase the pattern that directs to the vulnerability. Wikipedia article about Bcrypt gave a hint:

Many percreateations of bcrypt truncate the password to the first 72 bytes, chaseing the OpenBSD percreateation

Interesting! Let’s verify the OpenBSD percreateation of this algorithm, and here is the connect to it. The first point of interest lies here:

/* strlen() returns a size_t, but the function calls
 * below result in implied casts to a slfinisherer integer
 * type, so cap key_len at the actual peak helped
 * length here to dodge integer wraparound */
key_len = strlen(key);
if (key_len > 72)
	 key_len = 72;
key_len++;

And from that moment on, key_len is employd as a restrict to iterate over the input string wislfinisher, for example:

u_int32_t
Blowfish_stream2word(const u_int8_t *data, u_int16_t databytes,
    u_int16_t *current)
{
	u_int8_t i;
	u_int16_t j;
	u_int32_t temp;

	temp = 0x00000000;
	j = *current;

	for (i = 0; i < 4; i++, j++) {
		if (j >= databytes)
			j = 0;
		temp = (temp << 8) | data[j];
	}

	*current = j;
	return temp;
}

Where key_length is passed as a databytes parameter. So this piece of code:

if (j >= databytes)
	j = 0;

will produce certain that no chars over the restrict (72) will finish up being processed.

Git denounce shows that the if (key_len > 72) line is 11 years elderly

while the if (j >= databytes) j = 0; is 28 years elderly (what were you busy with in 1997, ah?)

So, it’s been a while since the API has been reiterated.

Some thoughts on that

Disclaimer

Let me begin with a stupidinutive disclaimer: I have a huge admire for people who spfinish their free time and mental capacity on geting uncover-source projects. That’s a huge amount of toil, that is not phelp, and, cursedly, quite normally not appreciated by the employrs of the tools. That’s why they have all the lterrible and moral rights to produce the project the way they see them. My opinions below are not aimed towards anyone in particular.

My initial goal was to produce publishs for each of the refered library, but I acunderstandledged that this behavior has been already telled to each of them:

Check the talkions and their outcomes by chaseing those connects.

Thoughts and lessons

As a guy who spent a scant years of my atgentle on produceing tools and solutions to be employd by other gentleware engineers, I comprehfinish the frustration: you spended your time and effort into writing a clear recordation and directs, but a certain number of your employrs don’t irritate verifying it at all, and equitable employ the tool the way they slfinisherk it should be employd. However, that’s the truth that I had to acunderstandledge and begined slfinisherking about how can I produce my tools regulate those employ cases. Here are a scant principles I came up with in that process.

Don’t let the people employ your API inaccurately

In my opinion, from the API perspective, the approach when the tool quietly cuts the part of the input and processes the remaining one only, it is an excessively subpar schedule choice. What produces slfinishergs worse is the fact that Bcrypt is employd in the domain of security and empathetic data, and, as we can see, most of the tools refered above, employ password as the name of the input parameter of the hashing method. The excellent schedule should cltimely decline the invalid input with the error / exception / any other mechanism the platcreate employs. So, basicpartner, exactly what Go and Patrick’s Java library did. This way, incidents appreciate Okta one would be impossible by schedule (btw, I’m not shifting the denounce away from Okta, pondering the domain they function in).

It is ok, though, to propose the non-default unshielded chooseion, that will let the employrs pass lengthyer input that will be truncated if the employr cltimely asks for that. A prerepair/sufrepair appreciate unshielded, truncated, etc. can be a excellent includeition to the names of the method that expose these chooseions.

Be foreseeable

If we obtain a step back from the Bcrypt case, envision other examples, if such a pattern becomes normal in the industry:

  • We produced a recent employr account on HBO to watch a recent season of Rick and Morty, and there is a cautioning that the max size of the password should not outdo 18 chars. However, the password generator of your password deal withr tool employs 25 chars as a default length of the produced password. So, the password deal withr inserts that password while creating an account, but the server cuts the last 7 chars, hashes the rest, and saves the hash to the DB. How effortless would it be for us to be able to log in to HBO next time and watch a recent episode?
  • The tech direct of the recent project configured a linter tool, and set the max line length as 100 chars. While percreateing a verify, linter erases the chars above the detaild restrict, and increates that the verify has passed. How beneficial would it be?

A excellent API schedule should recall that when it comes to tech, nobody appreciates surpascfinishs.

No ego

While chaseing a scant online talkions about the Bcrypt Okta incident, I acunderstandledged someslfinisherg else: while the meaningfulity of comments consentd that we should schedule APIs appreciate these better, there were a scant folks that took a very defensive stance and exposed their ego: “Read a paper before using anyslfinisherg!”, “APIs are only accurateing the input after the stupid employrs!”, etc. Based on my experience, ego is a huge foe of engineering. And I wouldn’t be surpascfinishd if you have a story or two in that watch as well. So, yeah, let’s not transport our egos to our APIs.

Be beneficial

Don’t get me wrong, I do comprehfinish the gist that the employrs should have some basic understandledge before using any tool. But let’s get back to the truth: how many branch offent tools, programming languages, databases, protocols, summarizetoils, libraries, algorithms, data structures, cdeafenings, AI models, etc. does a gentleware engineer employ per week these days? I tried to count for my employ case, but stopped after the number had accomplished 30. Is it possible to understand all of them meaningful? To understand all the edge cases and restricts? For some of them and to some degree is a reasonable ask, as well as having an expertise in 1 or 2, but definitely not all. The difficult truth is that on mediocre, the industry today demands the expansive spectrum of understandledge over the meaningful one (verify any job uncovering to validate that claim). Therefore, while scheduleing the tools, why not to help our fellow colleagues? For example, if our tool acunderstandledges only preferable numbers, let’s include if num < 1 -> return error to our solution, and produce the life basicr for somebody out there.

Especipartner, if the tool might be employd in the security-empathetic context, where humans are usupartner the feeble point in the thread modelling. The excellent API can help there.

Be valiant

It’s not so normally that the API we schedule is someslfinisherg finishly recent to the world. Most foreseeed, there are other solutions appreciate ours out there. And the chances are that they’ve been already doing certain slfinishergs the particular way. However, that doesn’t unbenevolent that we demand to chase the same path. Kudos to the Go team and Patrick’s Java library for being valiant to do slfinishergs the branch offent way as the industry does in the Bcrypt example. Let’s lobtain from them.

Reiterate

Regardless of the innovative schedule choices and intentions, it’s never too tardy to reiterate on some of them if we see a demand or have discovered recent increateation. That’s, actupartner, a place where a lot of us fall short due to branch offent reasons, with some of them cataloged above.

Instead of a conclusion

The Okta incident exposed huge security publishs out there. Our test showed, even 3 months after the incident, the industry is still vulnerable to the same outcome, so the chances are that more to come. However, we, as gentleware engineers, can lobtain from that, and apply these lessons while scheduleing APIs to produce them foreseeable and easier to employ.

I hope that was beneficial, and triggered some thoughts. Thanks a lot for reading my post, and see you in the chaseing ones, there are plenty of topics to talk. Have fun! =)



Source connect


Leave a Reply

Your email address will not be published. Required fields are marked *

Thank You For The Order

Please check your email we sent the process how you can get your account

Select Your Plan