LastPass disclosed yesterday that the copied vault backups hold website URLs in the clear alongside fully encrypted usernames, passwords and secure notes. This is about the two decisions that sentence records: where the encryption boundary sits in a record, and an iteration count stamped on an account at signup and never revisited. Both are made early by reasonable engineers, and a copied file gives the attacker unlimited offline guesses.
23 December 2022·8 min read·securitydata
The notice LastPass published yesterday contains one sentence that is worth more than the rest of the document put together. The copied backup, it says, holds vault data in a proprietary binary format containing both unencrypted data, such as website URLs, as well as fully-encrypted sensitive fields such as website usernames and passwords, secure notes, and form-filled data.
Read that as an engineer and it stops being a disclosure and becomes a schema. Two lists, drawn up by somebody at a whiteboard some years ago: the columns that go through the encryption path and the columns that do not. The second list was not an oversight, and I want to be careful about that from the start. Every field on it is out there because a feature needed to operate on it without a key, and the people who put it there could have named the feature.
The rest of the notice is the part everyone will quote. 256-bit AES, zero knowledge, the master password never held by the vendor, and what the notice calls a stronger-than-typical implementation of 100,100 iterations of PBKDF2. All of that is accurate as far as I can tell, and none of it is doing the work people think it is doing, because it describes the fields on the first list.
So this is a piece about two decisions that are made early, quietly, by engineers with reasonable motives, and then never revisited. One is where the encryption boundary sits in the record. The other is a per-account number that gets stamped on a row at signup and is inherited by that account forever. Neither is on a security dashboard. Both of them are the security.
Anything you search on has to leave the envelope. An encrypted blob is opaque by construction, which is the point of it and also the end of your query planner. If a user can type three letters and see matching entries, either the client holds the whole vault and searches it in memory, or something searchable lives server-side. The first choice is correct and expensive, and it gets harder every year the average vault grows. The second is a column of plaintext with a very good justification attached to it.
Anything the business counts leaves too. Adoption metrics, abuse detection, breached-credential matching, the report that says which categories of site users store most. Each of these is a legitimate need and each needs a field it can aggregate over. This is the quietest way the envelope shrinks, because the request does not arrive as a security decision. It arrives as a dashboard, and the person who adds the column is not the person who drew the boundary.
And so does anything that predates the boundary. Most encryption models are retrofitted onto a schema that already existed, and the retrofit gets applied to the fields somebody names as secret. Everything already in the table that was not named stays where it is, because migrating it means re-encoding every historical record with a key you do not hold. The boundary ends up drawn around the fields a person thought of on the day, and afterward it looks deliberate.
A list of every site a person keeps a credential for is a description of that person, and it is a better one than most things you could ask them directly. It says which bank, which brokerage, which health provider, which government portal, which employer's single sign-on host, which small internal tool at a company whose name is in the hostname. It does not need to be joined against anything, because it is already joined: the same backup carries the names, billing addresses, phone numbers and email addresses that go with it.
The engineering consequence is a change of category rather than a change of degree. An undifferentiated dump of encrypted blobs is a problem you can describe statistically. A dump with a plaintext index is a sorted list, and sorting is what turns a pile into a target list. The entries pointing at a brokerage get attention first. That is not a claim about anyone's intent. It is what anybody does with a dataset that has a useful column in it.
I do not think the people who left URLs outside the envelope were careless, and I have made the same call. The honest framing is that a URL felt like an address rather than a secret, and addresses feel public. The test that catches this is not "is this field sensitive" - almost nothing passes that test in isolation. It is "what does the set of these look like, for one person, all at once", and a schema review is exactly the wrong forum for that question, because it looks at one column at a time.
A derivation function sells time, by the guess. Turning a password into a key is deliberately slow. The iteration count is the price the attacker pays per guess, so doubling it doubles the cost of the entire search rather than adding to it. That is the whole mechanism and it is a good one. It also means the parameter is not a preference or a hardening option. It is the exchange rate between one attacker's hardware budget and how long your data stays closed.
The count lives on the account, not on the product. Because the key is derived on the client from a password the vendor does not hold, the parameters have to travel with the account so the client knows how to reproduce the key. So it is a column: a number sitting on a row, written when the row was written. There is no global setting to change. There is a default that applies to rows created after somebody changed it.
Defaults rise, and accounts do not follow. The recommended figures for PBKDF2 go up every few years because the hardware available to whoever is guessing gets faster and cheaper, and published guidance in this period puts the recommended count in the hundreds of thousands. A vendor raising its default is doing the right thing and is doing it only for new signups. Every account created under an older regime keeps the number it was given, which is the exact opposite of how everyone assumes security settings work.
Only the client can fix it, and only while unlocked. Re-deriving the key at a higher cost requires the password, which exists in plaintext for a few milliseconds inside the client at unlock and nowhere else, ever, by design. So the migration has to happen in the client, at login, silently, and then re-encrypt and upload the vault. That is a real feature with a real failure mode, not a configuration change, and the zero-knowledge property that makes the product worth using is precisely what makes it expensive.
So the oldest accounts are the weakest ones. Follow that through and the parameter is inversely correlated with tenure. The account created last month has current parameters and a handful of entries. The account created a decade ago has the oldest parameters, the most entries, the longest history and the most valuable contents. Whatever else this is, it is a system in which the customers who committed earliest are protected least, and nothing in the product surfaces that to them or to anyone.
Encryption at rest defends against somebody who obtains the storage and not the key, and against every attack that has to go through your front door, where you get rate limits, lockouts, anomaly detection and the ability to revoke. Once a backup has been copied, all of that machinery is on the wrong side of the problem. What remains is an attacker with the ciphertext, on hardware they own, running as many guesses in parallel as they care to buy, with no observer and no clock.
The vendor's own sentence is the honest one and I would have written it: if you use the default settings above, it would take millions of years to guess your master password using generally-available password-cracking technology. Every word of that is true and the load is carried by two conditions. Default settings, which an account created years ago may not have. Generally-available technology, which means today's, and the copy does not expire.
That last part is the piece I would put in front of a room. Exfiltrated ciphertext is not an incident with an end date. It sits on somebody's disk while the cost per guess falls year after year, so a parameter that buys a comfortable margin now buys less every year against the same file, and there is no patch, no rotation and no revocation that reaches it. The only variables left are the entropy of the password and the iteration count, and both were fixed before anyone knew this would happen.
Which is why I would not describe what happened here as an encryption failure. The encryption did its job on the fields it was given. What failed is quieter: a boundary that was drawn once and drifted outward as features arrived, and a parameter that was correct when it was chosen and was never given a way to become correct again.
Write down what is outside the envelope. Not what is encrypted, which everyone can recite. The other list: every column stored in the clear on an otherwise protected record, with the feature that requires it named next to it. If nobody can produce that list in a meeting, the boundary is not a design, it is the accumulated residue of individual pull requests, and it has been moving in one direction the whole time.
Make the derivation parameters a versioned, migratable thing. Store the parameter set with a version, not as a bare integer, and build the upgrade-on-next-unlock path in the same sprint as the original. It is a day of work when the schema is new and a quarter of work with a migration plan attached once there are millions of rows, and the difference between those two numbers is the entire reason the second one never happens.
Raising the iteration count for existing users is not free for those users. The derivation runs on the weakest device somebody owns, and the cost you are imposing on an attacker with a rack of hardware is also imposed on a five-year-old phone unlocking a vault at a checkout page. Every published recommendation lives above what vendors actually ship, and the gap is not laziness. It is the unlock time on the slowest device in the install base, which is a real constraint and not one a cryptographer has to hold.
The thing I would change is not the decision to defer the migration but what the decision is made against. The risk here is not spread evenly across the user base; it is concentrated on the oldest accounts, which correlate with the largest vaults and the longest tenure. Ranked that way, it is not "improve a parameter for everyone", it is "the customers who have trusted us longest are the ones we protect least, and here is how many of them there are". That is a fundable sentence. The first one never was.
I want to be careful about what I am not saying. The disclosure does not publish how iteration counts are distributed across accounts, and I am not going to guess at it from the outside, because the number would be invented and inventing it would be the same error as the confident claims that are going to fill the next week. Nor is any of this evidence that the design was bad. On the specific fields it was asked to protect, it did what it said, with a well-chosen algorithm and a parameter above what most of the industry ships.
What stays with me is how ordinary both of the failures are. A boundary drawn on a whiteboard once, around the fields somebody thought of that afternoon, with everything else left outside because it was useful there. A number chosen to keep unlock fast on a phone that has since been recycled, stamped onto accounts that will outlive the phone by twenty years. Neither has an owner, neither appears on any dashboard, and both of them decide what your encryption is actually worth on the day somebody walks off with a copy.