How the Lastpass Breach affects Lastpass SSO

Chaim Sanders
8 min readJan 6, 2023

Every week, almost without fail, I come across one thing that confuses, entertains, or most commonly infuriates me. I’ve decided to keep a record of my adventures.

Most folks at this point are familiar with the recent Lastpass Breach where attackers are known to have exfiltrated Lastpass encrypted user vaults. While Lastpass is known for their consumer product, many may not realize that Lastpass actually supports a number of SSO technologies. In an SSO environment, users don’t have passwords (or ‘Master Passwords’) so what is used for decryption of these Lastpass volumes? Let’s dig in!

Background

Lastpass and other password storage companies are a prime target for attackers. This is not a surprise to anyone, and their business is therefore highly dependent on ensuring security. To this end they rely (in part) on encryption to help secure their customer data from both Lastpass employees and external attackers. Cryptographers will tell you that encryption is only secure for a period of time, or security window, and depending on the attackers and their tactics that window can increase or shrink.

Even though AES-256, is known to be quite resilient to attacks, it can be said that the security window afforded to Lastpass users has been reduced substantially — for example, previously the most efficient method to brute forcing a vault (short of being employed at Lastpass) was via network requests. Now, attackers with access to vaults can conduct local attacks which can be highly parallelized and can take advantage of built-in AES co-processors and GPUs.

The Problem

In response to this latest breach, Lastpass has publicly released guidance that indicates that vaults are “secured with 256-bit AES encryption and can only be decrypted with a unique encryption key derived from each user’s master password using our Zero Knowledge architecture. As a reminder, the master password is never known to LastPass and is not stored or maintained by LastPass”

While this guidance is most directly targeted at Lastpass consumer customers, Lastpass also has a business offering that commonly uses SSO support. So, is this correct? Well…. yes, but it is more complex than that.

This issue becomes more pronounced when one talks about rotation of secrets upon compromise or possible compromise (like what has happened). But before we get too far ahead of ourselves let’s take a look at how this all works.

The Solution

How it Works

In this case I’m going to discuss Lastpass’ Okta SSO integration because I’m most familiar with it. However, given that Lastpass uses OAuth/OIDC for authentication I think you’ll agree is a pretty representative integration.

Lastpass provides a technical implementation whitepape that describes how a Lastpass vault passwords are generated (which is the same for all SSO integrations)

Hidden Master Password = base64(SHA256(K1 XOR K2))

This equation defines K1 as the company wide secret and K2 as the user generated secret.

Importantly, K2 is generated during the initial encryption of the vault and is subsequently stored with Lastpass. We will see that the requests to https://accounts.lastpass.com/federatedlogin/api/v1/getkey can be used to retrieve K2 (with an id_token).

K1 is a company-wide code stored within Okta. You’d be forgiven if you casually read that documentation and accompanying picture and anticipated that the client makes a request to Okta, but that’s not exactly it.

Let me explain, when setting up Lastpass SSO integration, the administrator is expected to add the Lastpass K1 value as an OAuth Access Token claim

All access tokens generated by this Authorization Server will contain the custom claim with the LastpassK1 value. However, the real question is how this access token gets passed to the Lastpass client, the answer here is simple: The OAuth implicit (or Authorization Code + PKCE) grant flow.

The Lastpass flow appears as follows (Please Note: Lastpass supports both the Implicit flow and Authorization Code + PKCE. Lastpass correctly recommends the use of authorization Code + PKCE flow — I’ll outline both):

1.Navigate to lastpass.com/lmiapi/login/type?username=[Email]

2a. (Implicit) Redirect to Okta OAuth Authorization Server requesting an OIDC ID Token

http://tenant.okta.com/oauth2/[AuthServerId]/v1/authorize?client_id=[OktaAppClientId]&redirect_uri=https://accounts.lastpass.com/federated/oidcredirect.html&response_type=id_token%20token&scope=openid%20email%20profile&state=[CSRFStateToken]&nonce=[OIDCNonceValue]&login_hint=[Email]

2b. (Auth Code) Redirect to Okta OAuth Authorization Server requesting a code and subsequent OIDC ID Token

https://tenant.oktapreview.com/oauth2/[AuthServerId]/v1/authorize?client_id=[OktaAppClientId]&redirect_uri=https://accounts.lastpass.com/federated/oidcredirect.html&response_type=code&scope=openid email profile&state=[CSRFStateToken]&code_challenge=[CodeChallenge]&code_challenge_method=S256&response_mode=fragment&login_hint=[Email]&nonce=[OIDCNonceValue]

3a. (Implicit) Okta responds by redirecting the user to https://accounts.lastpass.com/federated/oidcredirect.html with the Access Token and ID token passed via the fragment portion of the URL this is default when response_mode isn’t specified.

3b. (Auth Code) Okta responds by redirecting the user to https://accounts.lastpass.com/federated/oidcredirect.html with the Code and State passed via the fragment portion of the URL (Author’s Note: In Auth Code + PKCE this code will be useless without the code_verifier).

4. (Auth Code) The browser extension (which is watching all of this) inspects the fragments of the requests to https://accounts.lastpass.com/federated/oidcredirect.html and extracts the Code, and State, verifies the state, and then makes a request to https://tenant.oktapreview.com/oauth2/[AuthServerId]/v1/token with the code, code_verifier, client_id, grant_type, and redirect_uri. IMPORTANT NOTE: because the browser extension is doing the magic of generating the code_verifier and code_challenge in this model, the Auth Code flow does NOT support web-based login.

5. This page (or browser extension for Auth Code) makes a POST to https://tenant.okta.com/oauth2/[auth_server_id]/v1/userinfo with the Access Token to get user info (this may be to get additional user info, or it may be to ensure the Access Token is valid)

5. Then the page (or browser extension for Auth Code) makes a POST to https://accounts.lastpass.com/federatedlogin/api/v1/getkey which is passed the ID token.

6. /getkey responds with the value of the K2 in JSON format:

{"k2":"xxx","fragment_id":"xxx"}

While it may seem on first impression that the Access Token and ID Token are both sent to Lastpass, using the fragment response mode here actually means that the parameters are handled exclusively on the client side.

While this client does have access to the Access Token and uses it to make an Okta request, it never directly shares this value with lastpass.com, despite the value being passed to the user via accounts.lastpass.com in the implicit flow (funky right?).

An important note is that Lastpass needs to be really careful about how Okta (and probably other IDPs) change their API calls, for instance in the implicit flow they are banking that Okta never changes its default response mode handling.

Another interesting aspect here is that this is a very interesting case where Authorization Code + PKCE is not be able to easily replace the Implicit flow, in spite of various posts claiming that Implicit Flow is dead, because its leveraging a weakness of implicit flow to its benefit, lack of client authentication — i.e the Lastpass client may not originate the request. So as we’ve seen, by switching off the Implicit flow, Lastpass losses some functionality.

Security Window

The following is generated based on my observations but has been validated by others:

The K1 must be stored in an OIDC claim, which is a JWT and has limitations imposed across the various supported Authorization Servers, as a result it’s likely only printable, non-empty chars (space/delete), are acceptable. All observed generated K1’s were 33 characters long. This means that the K1 search space is 94³³ (~2²¹⁶).

K2s are returned in JSON bodies from Lastpass and are 44 characters long. From experimentation I was able to confirm these didn’t contain any special characters. This makes sense when considering that K1 and K2 are XOR’d and therefore need to be the same size. Considering that base64 expansion (ceil(n/3)*4) of a 33 character char value would result in a value of length 44, we can safely assume that this is a base64 encoded value. Depending on how you look at this, the search space can be 64⁴⁴ which is about 2²⁶³.

The nature of using an XOR in the equation makes either key useless without the other, unless the security of SHA256 was broken (which currently it isn’t) or the attacker had an intermediary step (the XORd key pre-SHA), which is only performed on the user’s machine.

Meanwhile, the SSO hidden password will always be a base64 encoded 32 byte (256-bit) value. This means that search space of all possible SSO master-keys for a given would be the same as the search space of K2. This would yield a final master password of aprox. 64⁴⁴ (~2²⁶³).

Please note, that just because brute forcing the Master Password takes on average the same time as brute forcing the K2 doesn’t mean that having a K1 or K2 compromised is fine. Remember that if your K1 is compromised (which should pretty much be assumed, see below), your K2s are ALL stored at Lastpass. Meanwhile if K2’s are compromised, K1’s are constant throughout the entire org and generally never change (see below).

The K1 and Rotation

Now there is a gotcha, compromising the K1 for one member of an organization compromises it for ALL previous, current, and future members of the organization. Any employee who uses Lastpass within an org has trivial access to this value (as it’s in the Okta Access Token).

You might think it is simply a matter of rotating the K1 after breach (and somewhat regularly) but it turns out that if this value is rotated all users will loose access to their vaults and the only way to reencrypt these vaults with the new K1 is to ‘defederate’ and ‘refederate’ the user.

This means that the K2 is the only real ‘secret’ (at least on a per-user basis). As discussed earlier, this value is stored in Lastpass which is known breached. At this time Lastpass has said that K2’s were not compromised but that might change.

Conclusion

Consumers affected by the Lastpass breach are encouraged to change their vault password. This helps with their security going forward, because if a weak master password was used (or a strong master password was brute forced), their current vault (which hopefully has lots of updated passwords) isn’t impacted.

Companies leveraging Lastpass with SSO don’t have this capability. Rotating either the K1 or K2 tokens effectively requires unenrolling and reenrolling users. This means that this Lastpass breach and other future potential Lastpass breaches (especially if K2s are compromised can result in compromise in a manner that cannot be easily recovered from.

With all that bad news there is some good news. If K2’s were in fact not compromised, the generation pattern for SSO master passwords means that they are not vulnerable to dictionary/rainbow table attacks that many other consumer vaults will likely be. So at least there is that.

--

--