Despite being open source, the .NET Bitwarden API code is somewhat difficult to navigate and comprehend from a high level, and there is no formal documentation on API endpoints or how the encryption and decryption is implemented.
The following notes were made by analyzing traffic between the Firefox
extension and the Bitwarden servers by running
tools/mitm.rb
and having the Firefox extension use http://127.0.0.1:4567/
as its
base URL.
The details for key derivation and encryption/decryption were done by reading the web extension code.
Overview:
- PBKDF2 with
$KdfIterations
rounds stretches the user's master password with a salt of the user's e-mail address to become the master key (unknown to the server). - 64 random bytes are generated to become the symmetric key, the first half of which becomes the encryption key and the second half becomes the MAC key.
- The master key and a random 16-byte IV are used to encrypt the symmetric key with AES-256-CBC. The output+IV become the "protected symmetric key" attached to the user's account, stored on the server and sent to the Bitwarden apps upon syncing.
- Private values for each string stored with an item (called "Cipher" objects) are encrypted with the user's symmetric key, which can only be known by decrypting the user's protected symmetric key with their master key. This encryption and decryption must be done entirely on the client side since the user's master password/key is never known to the server.
Changing the user's password or e-mail address will create a new master key, which is then used to re-encrypt the existing symmetric key, creating a new protected symmetric key while still being able to decrypt all existing Cipher object strings.
User enters a $masterPassword
of p4ssw0rd
and an $email
of
nobody@example.com
.
PBKDF2 is used with a password of $masterPassword
, salt of lowercased
$email
, and $iterations
KDF iterations to stretch password into
$masterKey
.
def makeKey(password, salt, iterations)
PBKDF2.new(:password => password, :salt => salt,
:iterations => iterations, :hash_function => OpenSSL::Digest::SHA256,
:key_length => (256 / 8)).bin_string
end
irb> $masterKey = makeKey("p4ssw0rd", "nobody@example.com".downcase, 5000)
=> "\x13\x88j`\x99m\xE3FA\x94\xEE'\xF0\xB2\x1A!\xB6>\\)\xF4\xD5\xCA#\xE5\e\xA6f5o{\xAA"
A random, 64-byte key $symmetricKey
is created to become the symmetric key.
The first 32 bytes become $encKey
and the last 32 bytes become $macKey
.
A random, 16-byte IV $iv
is created and $masterKey
is used as the key to
encrypt $symmetricKey
.
A "CipherString" (a Bitwarden internal format) is created by joining the
encryption type
(0
for AesCbc256_B64
), a dot, the Base64-encoded IV, and the Base64-encoded
$encKey
and $macKey
, with the pipe (|
) character to become
$protectedKey
.
def cipherString(enctype, iv, ct, mac)
[ enctype.to_s + "." + iv, ct, mac ].reject{|p| !p }.join("|")
end
# encrypt random bytes with a key to make new encryption key
def makeEncKey(key)
# pt[0, 32] becomes the cipher encryption key
# pt[32, 32] becomes the mac key
pt = OpenSSL::Random.random_bytes(64)
iv = OpenSSL::Random.random_bytes(16)
cipher = OpenSSL::Cipher.new "AES-256-CBC"
cipher.encrypt
cipher.key = key
cipher.iv = iv
ct = cipher.update(pt)
ct << cipher.final
return cipherString(0, Base64.strict_encode64(iv), Base64.strict_encode64(ct), nil)
end
irb> $protectedKey = makeEncKey($masterKey)
=> "0.uRcMe+Mc2nmOet4yWx9BwA==|PGQhpYUlTUq/vBEDj1KOHVMlTIH1eecMl0j80+Zu0VRVfFa7X/MWKdVM6OM/NfSZicFEwaLWqpyBlOrBXhR+trkX/dPRnfwJD2B93hnLNGQ="
This is now the main key associated with the user and sent to the server upon account creation, and sent back to the device upon sync.
An additional hash of the stretched password becomes $masterPasswordHash
and is also sent to the server upon account creation and login, to actually
verify the user account.
This hash is created with 1 round of PBKDF2 over a password of
$masterKey
(which itself was created by 5000 rounds of ($masterPassword
,
$email
)) and salt of $masterPassword
.
# base64-encode a wrapped, stretched password+salt for signup/login
def hashedPassword(password, salt, kdf_iterations)
key = makeKey(password, salt, kdf_iterations)
Base64.strict_encode64(PBKDF2.new(:password => key, :salt => password,
:iterations => 1, :key_length => 256/8,
:hash_function => OpenSSL::Digest::SHA256).bin_string)
end
irb> $masterPasswordHash = hashedPassword("p4ssw0rd", "nobody@example.com", 5000)
=> "r5CFRR+n9NQI8a525FY+0BPR0HGOjVJX0cR1KEMnIOo="
Upon future logins with the user's plain-text $masterPassword
and $email
,
$masterKey
can be calculated from them and then $masterPassword
should
be cleared from memory.
$protectedKey
returned from the server is then decrypted using $masterKey
,
revealing the $encKey
and $macKey
used for per-item encryption.
$masterPassword
and $masterKey
should never leave the device.
Bitwarden refers to individual items (site logins, secure notes, credit cards, etc.) as "cipher" objects, with its type value indicating what it is. Each cipher has a number of key/value pairs, with some values being encrypted:
{
"type": 1,
"folderId": null,
"organizationId": null,
"name":"2.zAgCKbTvGowtaRn1er5WGA==|oVaVLIjfBQoRr5EvHTwfhQ==|lHSTUO5Rgfkjl3J/zGJVRfL8Ab5XrepmyMv9iZL5JBE=",
"notes":"2.NLkXMHtgR8u9azASR4XPOQ==|6/9QPcnoeQJDKBZTjcBAjVYJ7U/ArTch0hUSHZns6v8=|p55cl9FQK/Hef+7yzM7Cfe0w07q5hZI9tTbxupZepyM=",
"favorite": false,
"login": {
"uris": [
{
"uri": "2.6DmdNKlm3a+9k/5DFg+pTg==|7q1Arwz/ZfKEx+fksV3yo0HMQdypHJvyiix6hzgF3gY=|7lSXqjfq5rD3/3ofNZVpgv1ags696B2XXJryiGjDZvk=",
"match": null
}
],
"username": "2.4Dwitdv4Br85MABzhMJ4hg==|0BJtHtXbfZWwQXbFcBn0aA==|LM4VC+qNpezmub1f4l1TMLDb9g/Q+sIis2vDbU32ZGA=",
"password": "2.OOlWRBGib6G8WRvBOziKzQ==|Had/obAdd2/6y4qzM1Kc/A==|LtHXwZc5PkiReFhkzvEHIL01NrsWGvintQbmqwxoXSI=",
"totp": null
}
}
The values for name
, notes
, login.uris[0].uri
, login.username
, and
login.password
are each encrypted as "CipherString" values, with the
leading 2
indicating its type (AesCbc256_HmacSha256_B64
).
To decrypt a value, the CipherString must be broken up into its IV, cipher
text, and MAC, then each part Base64-decoded. The MAC is calculated using
$macKey
and securely compared to the presented MAC, and if equal, the cipher
text is then decrypted using $encKey
:
# compare two hmacs, with double hmac verification
# https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
def macsEqual(macKey, mac1, mac2)
hmac1 = OpenSSL::HMAC.digest(OpenSSL::Digest.new("SHA256"), macKey, mac1)
hmac2 = OpenSSL::HMAC.digest(OpenSSL::Digest.new("SHA256"), macKey, mac2)
return hmac1 == hmac2
end
# decrypt a CipherString and return plaintext
def decrypt(str, key, macKey)
if str[0].to_i != 2
raise "implement #{str[0].to_i} decryption"
end
# AesCbc256_HmacSha256_B64
iv, ct, mac = str[2 .. -1].split("|", 3)
iv = Base64.decode64(iv)
ct = Base64.decode64(ct)
mac = Base64.decode64(mac)
cmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new("SHA256"), macKey, iv + ct)
if !macsEqual(macKey, mac, cmac)
raise "invalid mac"
end
cipher = OpenSSL::Cipher.new "AES-256-CBC"
cipher.decrypt
cipher.iv = iv
cipher.key = key
pt = cipher.update(ct)
pt << cipher.final
pt
end
irb> decrypt("2.6DmdNKlm3a+9k/5DFg+pTg==|7q1Arwz/ZfKEx+fksV3yo0HMQdypHJvyiix6hzgF3gY=|7lSXqjfq5rD3/3ofNZVpgv1ags696B2XXJryiGjDZvk=", $encKey, $macKey)
=> "https://example.com/login"
Encryption of a value is done by generating a random 16-byte IV $iv
and
using the key $encKey
to encrypt the text to $cipherText
.
The MAC $mac
is computed over ($iv + $cipherText)
.
$iv
, $cipherText
, and $mac
are each Base64-encoded, joined by a pipe
(|
) character, and then appended to the type (2
) and a dot to form a
CipherString.
# encrypt+mac a value with a key and mac key and random iv, return cipherString
def encrypt(pt, key, macKey)
iv = OpenSSL::Random.random_bytes(16)
cipher = OpenSSL::Cipher.new "AES-256-CBC"
cipher.encrypt
cipher.key = key
cipher.iv = iv
ct = cipher.update(pt)
ct << cipher.final
mac = OpenSSL::HMAC.digest(OpenSSL::Digest.new("SHA256"), macKey, iv + ct)
cipherString(2, Base64.strict_encode64(iv), Base64.strict_encode64(ct), Base64.strict_encode64(mac))
end
irb> encrypt("A secret note here...", $encKey, $macKey)
=> "2.NLkXMHtgR8u9azASR4XPOQ==|6/9QPcnoeQJDKBZTjcBAjVYJ7U/ArTch0hUSHZns6v8=|p55cl9FQK/Hef+7yzM7Cfe0w07q5hZI9tTbxupZepyM="
By default, BitWarden uses three different subdomains of bitwarden.com
, one
as the $baseURL
which does most API operations, one as the $identityURL
which handles logins (but not signups for some reason) and issues OAuth tokens,
and an $iconURL
which just fetches, caches, and serves requests for site
icons.
When configuring a self-hosted environment in the device apps before logging in, all three of these are assumed to be the same URL.
Collect an e-mail address and master password, calculate $internalKey
,
$masterPasswordHash
, and the $key
CipherString from the two values:
irb> $internalKey = makeKey("p4ssw0rd", "nobody@example.com".downcase, 5000)
=> "\x13\x88j`\x99m\xE3FA\x94\xEE'\xF0\xB2\x1A!\xB6>\\)\xF4\xD5\xCA#\xE5\e\xA6f5o{\xAA"
irb> $masterPasswordHash = hashedPassword("p4ssw0rd", "nobody@example.com", 5000)
=> "r5CFRR+n9NQI8a525FY+0BPR0HGOjVJX0cR1KEMnIOo="
irb> $key = makeEncKey($internalKey)
=> "0.uRcMe+Mc2nmOet4yWx9BwA==|PGQhpYUlTUq/vBEDj1KOHVMlTIH1eecMl0j80+Zu0VRVfFa7X/MWKdVM6OM/NfSZicFEwaLWqpyBlOrBXhR+trkX/dPRnfwJD2B93hnLNGQ="
Securely erase $masterPassword
from memory, as it is no longer needed until
the next login.
Issue a POST
to $baseURL/accounts/register
with a JSON body containing the
e-mail address, $masterPasswordHash
, KDF iteration count $kdfIterations
,
and $key
(not $internalKey!):
POST $baseURL/accounts/register
Content-type: application/json
{
"name": null,
"email": "nobody@example.com",
"masterPasswordHash": "r5CFRR+n9NQI8a525FY+0BPR0HGOjVJX0cR1KEMnIOo=",
"masterPasswordHint": null,
"key": "0.uRcMe+Mc2nmOet4yWx9BwA==|PGQhpYUlTUq/vBEDj1KOHVMlTIH1eecMl0j80+Zu0VRVfFa7X/MWKdVM6OM/NfSZicFEwaLWqpyBlOrBXhR+trkX/dPRnfwJD2B93hnLNGQ=",
"kdf": 0,
"kdfIterations": 5000,
}
The response should be a 200
with a zero-byte body.
Collect an e-mail address and master password, and issue a POST
to
$baseURL/accounts/prelogin
to determine the KDF iterations for the given
e-mail address:
POST $baseURL/accounts/prelogin
Content-type: application/json
{
"email": "nobody@example.com",
}
The prelogin
response will contain the KDF iteration count:
{
"Kdf": 0,
"KdfIterations": 5000,
}
With the KDF iteration count known, calculate $internalKey
and
$masterPasswordHash
from the three values:
irb> $internalKey = makeKey("p4ssw0rd", "nobody@example.com".downcase, 5000)
=> "\x13\x88j`\x99m\xE3FA\x94\xEE'\xF0\xB2\x1A!\xB6>\\)\xF4\xD5\xCA#\xE5\e\xA6f5o{\xAA"
irb> $masterPasswordHash = hashedPassword("p4ssw0rd", "nobody@example.com", 5000)
=> "r5CFRR+n9NQI8a525FY+0BPR0HGOjVJX0cR1KEMnIOo="
Securely erase the master password from memory, as it is no longer needed until the next login.
Issue a POST
to $identityURL/connect/token
(not $baseURL
which may be
different).
The deviceIdentifier
is a random UUID generated by the device and remains
constant across logins.
deviceType
is 3
for Firefox.
POST $identityURL/connect/token
Content-type: application/x-www-form-urlencoded
{
"grant_type": "password",
"username": "nobody@example.com",
"password": "r5CFRR+n9NQI8a525FY+0BPR0HGOjVJX0cR1KEMnIOo=",
"scope": "api offline_access",
"client_id": "browser",
"deviceType": 3
"deviceIdentifier": "aac2e34a-44db-42ab-a733-5322dd582c3d",
"deviceName": "firefox",
"devicePushToken": ""
}
A successful login will have a 200
status and a JSON response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkJDMz[...](JWT string)",
"expires_in": 3600,
"token_type": "Bearer",
"refresh_token": "28fb1911ef6db24025ce1bae5aa940e117eb09dfe609b425b69bff73d73c03bf",
"Key": "0.uRcMe+Mc2nmOet4yWx9BwA==|PGQhpYUlTUq/vBEDj1KOHVMlTIH1eecMl0j80+Zu0VRVfFa7X/MWKdVM6OM/NfSZicFEwaLWqpyBlOrBXhR+trkX/dPRnfwJD2B93hnLNGQ=",
}
If 2FA is enabled on the account (which has to be done through the Bitwarden
website for bitwarden.com accounts, or some other mechanism for private
accounts), the return status will be 400
and the JSON response will contain
a non-empty TwoFactorProviders
array containing the
provider IDs
of available services:
{
"error": "invalid_grant",
"error_description": "Two factor required.",
"TwoFactorProviders": [ 0 ],
"TwoFactorProviders2": { "0" : null }
}
The Bitwarden apps will prompt for the 2FA token, and then attempt to login
again to $identityURL/connect/token
with the twoFactorProvider
and
twoFactorToken
values filled out:
POST $identityURL/connect/token
Content-type: application/x-www-form-urlencoded
{
"grant_type": "password",
"username": "nobody@example.com",
"password": "r5CFRR+n9NQI8a525FY+0BPR0HGOjVJX0cR1KEMnIOo=",
"scope": "api offline_access",
"client_id": "browser",
"deviceType": 3,
"deviceIdentifier": "aac2e34a-44db-42ab-a733-5322dd582c3d",
"deviceName": "firefox",
"devicePushToken": ""
"twoFactorToken": "123456",
"twoFactorProvider": 0,
"twoFactorRemember": 1,
}
Upon successful login to an account with 2FA, additional PrivateKey
and
TwoFactorToken
values are sent, but I'm not sure what these are for.
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkJDMz[...](JWT string)",
"expires_in": 3600,
"token_type": "Bearer",
"refresh_token": "28fb1911ef6db24025ce1bae5aa940e117eb09dfe609b425b69bff73d73c03bf",
"PrivateKey": "2.WAfJirrIw2vPRIYZn/IadA==|v/PLyfn3P1YKDdbRCd+40k3Z[...](very long CipherString)",
"Key": "0.uRcMe+Mc2nmOet4yWx9BwA==|PGQhpYUlTUq/vBEDj1KOHVMlTIH1eecMl0j80+Zu0VRVfFa7X/MWKdVM6OM/NfSZicFEwaLWqpyBlOrBXhR+trkX/dPRnfwJD2B93hnLNGQ=",
"TwoFactorToken": "CfDJ8MXkSBvqpelMmq7HvH8L8fsvRsCETUwZQeOOXh21leQs2PmyuvuxdlhT95S+Otmn63gl6FNqLDL2gCqSNB+fHWTqdlX38GSWvGJimuAUeLu3Xgrd2Y0bEzjoBW+3YV4mHJPGwIu/2CaWZl6JW4F229x8fwYbPhRADczligiG1EFxbFswRwmZqmSny5o0VgKUHLIiSDfl2elHYzVpkkKYBoysX9pQ1NoYa7IJJReaWYoP"
}
The access_token
, refresh_token
, and expires_in
values must be stored
and used for further API access.
$access_token
must be a
JWT
string, which the browser extension decodes and parses, and must have at least
nbf
, exp
, iss
, sub
, email
, name
, premium
, and iss
keys.
$access_token
is sent as the Authentication
header for up to $expires_in
seconds, after which the $refresh_token
will need to be sent back to the
identity server to get a new $access_token
.
The main action of the client is a one-way sync, which just fetches all objects from the server and updates its local database.
Issue a GET
to $baseURL/sync
with an Authorization
header of the
$access_token
.
GET $baseURL/sync
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkJDMz(rest of $access_token)
A successful response will contain a JSON body with Profile
, Folders
,
Ciphers
, and Domains
objects.
{
"Profile": {
"Id": "0fbfc68d-ba11-416a-ac8a-a82600f0e601",
"Name": null,
"Email": "nobody@example.com",
"EmailVerified": false,
"Premium": false,
"MasterPasswordHint": null,
"Culture": "en-US",
"TwoFactorEnabled": false,
"Key": "0.uRcMe+Mc2nmOet4yWx9BwA==|PGQhpYUlTUq/vBEDj1KOHVMlTIH1eecMl0j80+Zu0VRVfFa7X/MWKdVM6OM/NfSZicFEwaLWqpyBlOrBXhR+trkX/dPRnfwJD2B93hnLNGQ=",
"PrivateKey": null,
"SecurityStamp": "5d203c3f-bc89-499e-85c4-4431248e1196",
"Organizations": [
],
"Object": "profile"
},
"Folders": [
{
"Id": "14220912-d002-471d-a364-a82a010cb8f2",
"Name": "2.tqb+y2z4ChCYHj4romVwGQ==|E8+D7aR5CNnd+jF7fdb9ow==|wELCxyy341G2F+w8bTb87PAUi6sdXeIFTFb4N8tk3E0=",
"RevisionDate": "2017-11-13T16:20:56.5633333",
"Object": "folder"
}
],
"Ciphers": [
{
"FolderId": null,
"Favorite": false,
"Edit": true,
"Id": "0f01a66f-7802-42bc-9647-a82600f11e10",
"OrganizationId": null,
"Type":1,
"Login":{
"Uris": [
{
"Uri": "2.6DmdNKlm3a+9k/5DFg+pTg==|7q1Arwz/ZfKEx+fksV3yo0HMQdypHJvyiix6hzgF3gY=|7lSXqjfq5rD3/3ofNZVpgv1ags696B2XXJryiGjDZvk=",
"Match": null,
},
],
"Username": "2.4Dwitdv4Br85MABzhMJ4hg==|0BJtHtXbfZWwQXbFcBn0aA==|LM4VC+qNpezmub1f4l1TMLDb9g/Q+sIis2vDbU32ZGA=",
"Password":"2.OOlWRBGib6G8WRvBOziKzQ==|Had/obAdd2/6y4qzM1Kc/A==|LtHXwZc5PkiReFhkzvEHIL01NrsWGvintQbmqwxoXSI=",
"Totp":null,
},
"Name": "2.zAgCKbTvGowtaRn1er5WGA==|oVaVLIjfBQoRr5EvHTwfhQ==|lHSTUO5Rgfkjl3J/zGJVRfL8Ab5XrepmyMv9iZL5JBE=",
"Notes": "2.NLkXMHtgR8u9azASR4XPOQ==|6/9QPcnoeQJDKBZTjcBAjVYJ7U/ArTch0hUSHZns6v8=|p55cl9FQK/Hef+7yzM7Cfe0w07q5hZI9tTbxupZepyM=",
"Fields": null,
"Attachments": null,
"OrganizationUseTotp": false,
"RevisionDate": "2017-11-09T14:37:52.9033333",
"Object":"cipher"
}
],
"Domains": {
"EquivalentDomains": null,
"GlobalEquivalentDomains": [
{
"Type": 2,
"Domains": [
"ameritrade.com",
"tdameritrade.com"
],
"Excluded": false
},
[...]
],
"Object": "domains"
},
"Object": "sync"
}
After $expires_in
seconds of login (or last refresh), the $access_token
expires and has to be refreshed.
Send a POST
request to the identity server with the $refresh_token
and
get a new $access_token
in return.
POST $identityURL/connect/token
Content-type: application/x-www-form-urlencoded
{
"grant_type": "refresh_token",
"client_id": "browser",
"refresh_token": "28fb1911ef6db24025ce1bae5aa940e117eb09dfe609b425b69bff73d73c03bf",
}
A successful response will contain a JSON body with a new $access_token
and the same $refresh_token
.
{
"access_token": "(new access token)",
"expires_in": 3600,
"token_type": "Bearer",
"refresh_token": "28fb1911ef6db24025ce1bae5aa940e117eb09dfe609b425b69bff73d73c03bf",
}
When a new item (login, secure note, etc.) is created on a device, it is
sent to the server via a POST
to $baseURL/ciphers
:
POST $baseURL/ciphers
Content-type: application/json
Authorization: Bearer $access_token
{
"type": 1,
"folderId": null,
"organizationId": null,
"name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=",
"notes": null,
"favorite": false,
"login": {
"uri": "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=",
"username": "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=",
"password": "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=",
"totp": null
}
}
With no errors, the server will send back a JSON response with the cipher data:
{
"FolderId": null,
"Favorite": false,
"Edit": true,
"Id": "4c2869dd-0e1c-499f-b116-a824016df251",
"OrganizationId": null,
"Type": 1,
"Login": {
"Uris": [
{
"Uri": "2.T57BwAuV8ubIn/sZPbQC+A==|EhUSSpJWSzSYOdJ/AQzfXuUXxwzcs/6C4tOXqhWAqcM=|OWV2VIqLfoWPs9DiouXGUOtTEkVeklbtJQHkQFIXkC8=",
"Match": null,
},
],
},
"Username": "2.JbFkAEZPnuMm70cdP44wtA==|fsN6nbT+udGmOWv8K4otgw==|JbtwmNQa7/48KszT2hAdxpmJ6DRPZst0EDEZx5GzesI=",
"Password": "2.e83hIsk6IRevSr/H1lvZhg==|48KNkSCoTacopXRmIZsbWg==|CIcWgNbaIN2ix2Fx1Gar6rWQeVeboehp4bioAwngr0o=",
"Totp": null,
"Name": "2.d7MttWzJTSSKx1qXjHUxlQ==|01Ath5UqFZHk7csk5DVtkQ==|EMLoLREgCUP5Cu4HqIhcLqhiZHn+NsUDp8dAg1Xu0Io=",
"Notes": null,
"Fields": null,
"Attachments": null,
"OrganizationUseTotp": false,
"RevisionDate": "2017-11-07T22:12:22.235914Z",
"Object": "cipher"
}
Send a PUT
request to $baseURL/ciphers/(cipher UUID)
:
PUT $baseURL/ciphers/(cipher UUID)
Content-type: application/json
Authorization: Bearer $access_token
{
"type": 2,
"folderId": null,
"organizationId": null,
"name": "2.G38TIU3t1pGOfkzjCQE7OQ==|Xa1RupttU7zrWdzIT6oK+w==|J3C6qU1xDrfTgyJD+OrDri1GjgGhU2nmRK75FbZHXoI=",
"notes": "2.rSw0uVQEFgUCEmOQx0JnDg==|MKqHLD25aqaXYHeYJPH/mor7l3EeSQKsI7A/R+0bFTI=|ODcUScISzKaZWHlUe4MRGuTT2S7jpyDmbOHl7d+6HiM=",
"favorite": true,
"secureNote":{
"type": 0
}
}
The JSON response will be the same as when creating a new item.
Send an empty DELETE
request to $baseURL/ciphers/(cipher UUID)
:
DELETE $baseURL/ciphers/(cipher UUID)
Authorization: Bearer (access_token)
A successful but zero-length response will be returned.
Send a POST
request to $baseURL/ciphers/(cipher UUID)/attachment
It is a multipart/form-data post, with the file under the data
-attribute the single posted entity.
POST $baseURL/ciphers/(cipher UUID)/attachment
Content-type: application/json
Authorization: Bearer $access_token
{
"data": {
"filename": "encrypted_filename"
"tempfile": blob
}
}
The JSON response will then be the complete cipher item, but now containing an entry for the new attachment:
{
"FolderId"=>nil,
...
"Data"=> ...,
"Attachments"=>
[
{ "Id"=>"7xytytjp1hc2ijy3n5y5vbbnzcukmo8b",
"Url"=> "https://cdn.bitwarden.com/attachments/(cipher UUID)/7xytytjp1hc2ijy3n5y5vbbnzcukmo8b",
"FileName"=> "2.GOkRA8iZio1KxB+UkJpfcA==|/Mc8ACbPr9CRRQmNKPYHVg==|4BBQf8YTbPupap6qR97qMdn0NJ88GdTgDPIyBsQ46aA=",
"Size"=>"65",
"SizeName"=>"65 Bytes",
"Object"=>"attachment"
}
],
...,
"Object"=>"cipher"
}
Send an empty DELETE
request to $baseURL/ciphers/(cipher UUID)/attachment/(attachment id)
:
DELETE $baseURL/ciphers/(cipher UUID)/attachment/(attachment id)
Authorization: Bearer (access_token)
A successful but zero-length response will be returned.
$cdn_url using the official server is https://cdn.bitwarden.com.
Send an unauthenticated GET
request to $cdn_url/attachments/(cipher UUID)/(attachment id)
:
GET $cdn_url/attachments/(cipher UUID)/(attachment id)
The file will be sent as a response.
To create a folder, POST
to $baseURL/folders
:
POST $baseURL/folders
Content-type: application/json
Authorization: Bearer $access_token
{
"name": "2.FQAwIBaDbczEGnEJw4g4hw==|7KreXaC0duAj0ulzZJ8ncA==|nu2sEvotjd4zusvGF8YZJPnS9SiJPDqc1VIfCrfve/o="
}
JSON response:
{
"Id": "14220912-d002-471d-a364-a82a010cb8f2",
"Name": "2.FQAwIBaDbczEGnEJw4g4hw==|7KreXaC0duAj0ulzZJ8ncA==|nu2sEvotjd4zusvGF8YZJPnS9SiJPDqc1VIfCrfve/o=",
"RevisionDate": "2017-11-13T16:18:23.3078169Z",
"Object": "folder"
}
To rename a folder, PUT
to $baseURL/folders/(folder UUID)
with the
same structure as the POST
, and get the same result.
To delete a folder, DELETE
to $baseURL/folders/(folder UUID)
and get a
successful, zero-length response.
Each login cipher can show an icon (favicon) for its URL, which is fetched via Bitwarden's servers (presumably for caching).
To fetch an icon for a URL, issue an unauthenticated GET
to
$iconURL/(domain)/icon.png
:
GET $iconURL/google.com/icon.png
(no authentication header)
The binary response will contain the icon.