Bitwarden is not only a password manager, it’s a password manager written in C# and open-source. So we can peak at the code to our heart’s content, and that’s exactly what I did.
What is a password manager?
For those of you unfamiliar with a password manager, it’s is an app that helps you generate, store and organise passwords in a secure manner. Generally, you’ll use a single master password to access all of your other passwords.
So for each website, you’ll generate a complicated password such as the following
aek$C!Z6k68hiA5pXArThU9PekPH7CCW9eZi2%$ak9pjKXs@UhZbXf2^59MGqp#CPnb&hry#k4STZNpoHfvVc3#omgSF!jQbBHWgsJRUT!tK8uompNvjxG#Y$h
which if you paste it into a website that calculates the time a computer would take to crack the password:

Which isn’t too bad. And then of course, when they finally stumble across the correct password they’ll notice that I’ve enabled 2FA, so they’ll also need access to my email, add another 3 trevigintillion years for that.
A good password manager will also have features such as autofill and two-factor auth. Password sharing is something that a good password manager facilitates, or you could just go with the good ol’ copy/paste job. Just recently I was sending my mum a password to an account we share together, the password was >= 128 characters with varying capitalisation, numbers, unique characters, etc… I tried to convince her I remember it by using mnemonics, she was impressed, for about 5 seconds ๐.
Why is it okay to use a single master password?
This was the first question I had when I heard about password managers. Having a single, long, complicated master password should be relatively easy for you to remember, but if somebody guesses your master password then they’ll have access to everything! And you’re in the same boat as you would have been if you used the same password everywhere.
There are a few reasons why this is preferable. The whole idea of a password manager is to allow you to have a single source of truth for all of your passwords and allow you to generate uncrackable passwords. So if somebody cracks your uncrackable Ebay password, they’ll also need to crack your Paypal password in order to make a purchase. If you weren’t using a password manager and have a complicated unique password for both Paypal and Ebay, how are you remembering those? Surely you’re not writing them down!
Bitwarden Server
The backend code of Bitwarden contains the APIs that the client applications consume. If you’re interested in how to set this up locally they have a pretty decent setup guide that’ll take you through step-by-step.
There’s two things I want to find out here:
- What is it doing with the master password?
- How is it encrypting/decrypting the passwords I store in the manager?
What is it doing with the master password?
When you sign up to Bitwarden you have to provide the master password. Let’s have a look at which endpoint it’s calling when I do this:

The javascript code to create the request looks like the following:
private async buildRegisterRequest(
email: string,
masterPassword: string,
name: string
): Promise<RegisterRequest> {
const hint = this.formGroup.value.hint;
const kdf = DEFAULT_KDF_TYPE;
const kdfIterations = DEFAULT_KDF_ITERATIONS;
const key = await this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
const encKey = await this.cryptoService.makeEncKey(key);
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
const keys = await this.cryptoService.makeKeyPair(encKey[0]);
const request = new RegisterRequest(
email,
name,
hashedPassword,
hint,
encKey[1].encryptedString,
kdf,
kdfIterations,
this.referenceData,
this.captchaToken
);
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
const orgInvite = await this.stateService.getOrganizationInvitation();
if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) {
request.token = orgInvite.token;
request.organizationUserId = orgInvite.organizationUserId;
}
return request;
}
So it is creating a key from the master password, email, kdf and kfiterations. KDF means key derivation function, this is defaulted to 100,000. So the master password is salted and hashed locally before it even hits the server. Let’s have a look at what the server does with it:
// Moved from API, If you modify this endpoint, please update API as well. Self hosted installs still use the API endpoints.
[HttpPost("register")]
[CaptchaProtected]
public async Task<RegisterResponseModel> PostRegister([FromBody] RegisterRequestModel model)
{
var user = model.ToUser();
var result = await _userService.RegisterUserAsync(user, model.MasterPasswordHash,
model.Token, model.OrganizationUserId);
if (result.Succeeded)
{
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
return new RegisterResponseModel(captchaBypassToken);
}
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
{
ModelState.AddModelError(string.Empty, error.Description);
}
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
Above is the endpoint, the RegisterUserAsync
contains the code for creating the user, here it calls to hash the password. Below you can see the HashPasswordV3
functions. The purpose of the function is to hash the password using the PBKDF2 key derivation function along with a randomnly generated salt and a specified number of iterations (which is 100,000 by default).
private byte[] HashPasswordV3(string password, RandomNumberGenerator rng)
{
return HashPasswordV3(password, rng,
prf: KeyDerivationPrf.HMACSHA256,
iterCount: _iterCount,
saltSize: 128 / 8,
numBytesRequested: 256 / 8);
}
private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
{
// Produce a version 3 (see comment above) text hash.
byte[] salt = new byte[saltSize];
rng.GetBytes(salt);
byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);
var outputBytes = new byte[13 + salt.Length + subkey.Length];
outputBytes[0] = 0x01; // format marker
WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
return outputBytes;
}
That’s settled then, my master password is in good hands.
How is it storing passwords for different websites?
So how is it storing my Passwords, first step is to figure out which endpoint is being called when I add a new item to my vault. To do this I simply play through a request and open up the network tab of the browser as before:

So it’s calling the POST /ciphers api with the following body:
{
"type": 1,
"folderId": null,
"organizationId": null,
"name": "2.JK5iEdQIfsds1mhh9gcxcQ==|xqZ6oUYvhhfsdSgO69azA==|AvTwizwrXfsd39UBhhW0yoD3Rasdfadsfdnwvs6IY=",
"notes": null,
"favorite": false,
"lastKnownRevisionDate": null,
"reprompt": 0,
"login": {
"response": null,
"uris": [
{
"response": null,
"match": null,
"uri": "2.4iWHhH71Uvy/Z/hhdasdfasdHw==|tkUl/Ab+Z8fdshhf1SyvKLghWfdFcasdfasdfgstYuc=|Sw8aetbh9ShtnWhh0xvcfdsaEahaIsdfasdfQXsc="
}
],
"username": "2.tiuCxSLMCMasdWqashhdfasBI/iA==|M+gLfjzIcxcvAlVphhlWNasdfas==|1SNd+jh8gr+fZrt/cxvxcwtWovcxvdctCc/tYcasNAVoA=",
"password": "2./oZYrGMKGkxcvwJ6adsfashhdYA==|0co7LXZuK2wvcxxcE/asdhhhsd==|rBAbGnY47JXjzF3phkn+ZaswhvxcvIrzJoEJGs5PwY=",
"passwordRevisionDate": null,
"totp": null,
"autofillOnPageLoad": null
}
}
(I’ve made up all the encrypted values above for obvious reasons) So the encryption has already happened before sending up the username and password. Let’s figure out what’s going on. I’ve not checked out the frontend code so in order to track back to where the cipher is being created I simply put in a breakpoint and worked my way back to this submit function (note i’ve ommitted some unimportant code):
async submit(): Promise<boolean> {
...
const cipher = await this.encryptCipher();
try {
this.formPromise = this.saveCipher(cipher);
await this.formPromise;
this.cipher.id = cipher.id;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem")
);
this.onSavedCipher.emit(this.cipher);
this.messagingService.send(this.editMode && !this.cloneMode ? "editedCipher" : "addedCipher");
return true;
} catch (e) {
this.logService.error(e);
}
return false;
}
So this submit function calls this.encryptCipher()
which forwards on the call to the cipherService
:
protected encryptCipher() {
return this.cipherService.encrypt(this.cipher);
}
At this point, this.cipher
is unencrypted, notice you can see my secure username and password testusername
and testpassword
:
{
"initializerKey": 1,
"id": null,
"organizationId": null,
"folderId": null,
"name": "test",
"notes": null,
"type": 1,
"favorite": false,
"organizationUseTotp": false,
"edit": false,
"viewPassword": true,
"login": {
"username": "testusername",
"password": "testpassword",
"passwordRevisionDate": null,
"totp": null,
"uris": null,
"autofillOnPageLoad": null
},
}
The encrpyt
code in the cipher.service.ts
looks like this (once again, I’ve ommitted some details for brevity):
async encrypt(
model: CipherView,
key?: SymmetricCryptoKey,
originalCipher: Cipher = null
): Promise<Cipher> {
// Adjust password history
...
const cipher = new Cipher();
cipher.id = model.id;
cipher.folderId = model.folderId;
cipher.favorite = model.favorite;
cipher.organizationId = model.organizationId;
cipher.type = model.type;
cipher.collectionIds = model.collectionIds;
cipher.revisionDate = model.revisionDate;
cipher.reprompt = model.reprompt;
cipher.edit = model.edit;
await Promise.all([
this.encryptObjProperty(
model,
cipher,
{
name: null,
notes: null,
},
key
),
this.encryptCipherData(cipher, model, key),
this.encryptFields(model.fields, key).then((fields) => {
cipher.fields = fields;
}),
this.encryptPasswordHistories(model.passwordHistory, key).then((ph) => {
cipher.passwordHistory = ph;
}),
this.encryptAttachments(model.attachments, key).then((attachments) => {
cipher.attachments = attachments;
}),
]);
return cipher;
}
A cipher is created using a load of information from the model, this is then passed along into the encryptCipherData()
method along with an optional key and the model itself. encryptObjProperty
then does the actual encrypting.
The encryptObjProperty
function is an interesting one. So we’ve passed through the model
all the way to here. The model is the object that contains the properties that are to be encrypted. The obj
is an object that will have the properties set to the encrypted values from the model
object. map
is an object that maps the properties of model
to the properties of obj
and key
is a symmetric key that is used in encryption.
private async encryptObjProperty<V extends View, D extends Domain>(
model: V,
obj: D,
map: any,
key: SymmetricCryptoKey
): Promise<void> {
const promises = [];
const self = this;
for (const prop in map) {
// eslint-disable-next-line
if (!map.hasOwnProperty(prop)) {
continue;
}
(function (theProp, theObj) {
const p = Promise.resolve()
.then(() => {
const modelProp = (model as any)[map[theProp] || theProp];
if (modelProp && modelProp !== "") {
return self.cryptoService.encrypt(modelProp, key);
}
return null;
})
.then((val: EncString) => {
(theObj as any)[theProp] = val;
});
promises.push(p);
})(prop, obj);
}
await Promise.all(promises);
}
So the property we want to encrypt is converted from a string to an ArrayBuffer
, and assigned to the plainBuf
variable.
async encrypt(plainValue: string | ArrayBuffer, key: SymmetricCryptoKey): Promise<EncString> {
if (key == null) {
throw new Error("No encryption key provided.");
}
if (plainValue == null) {
return Promise.resolve(null);
}
let plainBuf: ArrayBuffer;
if (typeof plainValue === "string") {
plainBuf = Utils.fromUtf8ToArray(plainValue).buffer;
} else {
plainBuf = plainValue;
}
const encObj = await this.aesEncrypt(plainBuf, key);
const iv = Utils.fromBufferToB64(encObj.iv);
const data = Utils.fromBufferToB64(encObj.data);
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
return new EncString(encObj.key.encType, data, iv, mac);
}
This is how the encrypted object is created. First it creates a random initialization vector (IV) using a randomBytes
method. The data
is then encrypted using the generated IV and the encryption key. It then creates a mac key using the hmac
method with the SHA-256 hash function.
private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise<EncryptedObject> {
const obj = new EncryptedObject();
obj.key = key;
obj.iv = await this.cryptoFunctionService.randomBytes(16);
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey);
if (obj.key.macKey != null) {
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
macData.set(new Uint8Array(obj.iv), 0);
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, "sha256");
}
return obj;
}
And lastly, for the frontend the encrypted string is generated, which is the string shown in the encrypted payload above:
private initFromData(encType: EncryptionType, data: string, iv: string, mac: string) {
if (iv != null) {
this.encryptedString = encType + "." + iv + "|" + data;
} else {
this.encryptedString = encType + "." + data;
}
// mac
if (mac != null) {
this.encryptedString += "|" + mac;
}
this.encryptionType = encType;
this.data = data;
this.iv = iv;
this.mac = mac;
}
So for the vault data Bitwarden uses AES-CBC (Cipher block chaining). It uses this along with the encryption key (the master password).
Conclusion
Bitwarden using AES-CBC for encrypting vault data using the encryption key from the master password. The master password is stored using PBKDF2, salting and hashing on the frontend and then salted and hashed again on the bitwarden server. It hashes โ by default โ 100, 001 times, 1 time on the frontend and then an additional 100,000 iterations on the backend.
And thatโs pretty much all there is to it, if you liked this blog then please sign up for my newsletter and join an awesome community!