···11## Bitwarden API Overview
2233Despite being open source, the
44-[Bitwarden API code](https://github.com/bitwarden/core)
44+[.NET Bitwarden API code](https://github.com/bitwarden/core)
55is somewhat difficult to navigate and comprehend from a high level,
66and there is no formal documentation on API endpoints or how the
77encryption and decryption is implemented.
···2525PBKDF2 is used with a password of `$masterPassword`, salt of lowercased
2626`$email`, and 5000 iterations to stretch password into `$internalKey`.
27272828+ def makeKey(password, salt)
2929+ PBKDF2.new(:password => password, :salt => salt,
3030+ :iterations => 5000, :hash_function => OpenSSL::Digest::SHA256,
3131+ :key_length => (256 / 8)).bin_string
3232+ end
3333+2834 irb> $internalKey = makeKey("p4ssw0rd", "nobody@example.com".downcase)
2935 => "\x13\x88j`\x99m\xE3FA\x94\xEE'\xF0\xB2\x1A!\xB6>\\)\xF4\xD5\xCA#\xE5\e\xA6f5o{\xAA"
30363131-An IV `$iv` is created of 16 random bytes and `$internalKey` is used as the
3737+An IV `$iv` is created with 16 random bytes and `$internalKey` is used as the
3238key to encrypt 64 random bytes.
3339The first 32 bytes of the result become `$encKey` and the last 32 bytes become
3440`$macKey`.
···3844(`0` for `AesCbc256_B64`), a dot, the Base64-encoded IV, and the Base64-encoded
3945`$encKey` and `$macKey`, with the pipe (`|`) character to become `$key`.
40464747+ def cipherString(enctype, iv, ct, mac)
4848+ [ enctype.to_s + "." + iv, ct, mac ].reject{|p| !p }.join("|")
4949+ end
5050+5151+ # encrypt random bytes with a key to make new encryption key
5252+ def makeEncKey(key)
5353+ pt = OpenSSL::Random.random_bytes(64)
5454+ iv = OpenSSL::Random.random_bytes(16)
5555+5656+ cipher = OpenSSL::Cipher.new "AES-256-CBC"
5757+ cipher.encrypt
5858+ cipher.key = key
5959+ cipher.iv = iv
6060+ ct = cipher.update(pt)
6161+ ct << cipher.final
6262+6363+ return cipherString(0, Base64.strict_encode64(iv), Base64.strict_encode64(ct), nil)
6464+ end
6565+4166 irb> $key = makeEncKey($internalKey)
4267 => "0.uRcMe+Mc2nmOet4yWx9BwA==|PGQhpYUlTUq/vBEDj1KOHVMlTIH1eecMl0j80+Zu0VRVfFa7X/MWKdVM6OM/NfSZicFEwaLWqpyBlOrBXhR+trkX/dPRnfwJD2B93hnLNGQ="
4368···5075This hash is created with 1 round of PBKDF2 over a password of
5176`$internalKey` (which itself was created by 5000 rounds of (`$masterPassword`,
5277`$email`)) and salt of `$masterPassword`.
7878+7979+ # base64-encode a wrapped, stretched password+salt for signup/login
8080+ def hashedPassword(password, salt)
8181+ key = makeKey(password, salt)
8282+ Base64.strict_encode64(PBKDF2.new(:password => key, :salt => password,
8383+ :iterations => 1, :key_length => 256/8,
8484+ :hash_function => OpenSSL::Digest::SHA256).bin_string)
8585+ end
53865487 irb> $masterPasswordHash = hashedPassword("p4ssw0rd", "nobody@example.com")
5588 => "r5CFRR+n9NQI8a525FY+0BPR0HGOjVJX0cR1KEMnIOo="
···93126`$macKey` and securely compared to the presented MAC, and if equal, the cipher
94127text is then decrypted using `$encKey`:
95128129129+ # compare two hmacs, with double hmac verification
130130+ # https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
131131+ def macsEqual(macKey, mac1, mac2)
132132+ hmac1 = OpenSSL::HMAC.digest(OpenSSL::Digest.new("SHA256"), macKey, mac1)
133133+ hmac2 = OpenSSL::HMAC.digest(OpenSSL::Digest.new("SHA256"), macKey, mac2)
134134+ return hmac1 == hmac2
135135+ end
136136+137137+ # decrypt a CipherString and return plaintext
138138+ def decrypt(str, key, macKey)
139139+ if str[0].to_i != 2
140140+ raise "implement #{str[0].to_i} decryption"
141141+ end
142142+143143+ # AesCbc256_HmacSha256_B64
144144+ iv, ct, mac = str[2 .. -1].split("|", 3)
145145+146146+ iv = Base64.decode64(iv)
147147+ ct = Base64.decode64(ct)
148148+ mac = Base64.decode64(mac)
149149+150150+ cmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new("SHA256"), macKey, iv + ct)
151151+ if !macsEqual(macKey, mac, cmac)
152152+ raise "invalid mac"
153153+ end
154154+155155+ cipher = OpenSSL::Cipher.new "AES-256-CBC"
156156+ cipher.decrypt
157157+ cipher.iv = iv
158158+ cipher.key = key
159159+ pt = cipher.update(ct)
160160+ pt << cipher.final
161161+ pt
162162+ end
163163+96164 irb> decrypt("2.6DmdNKlm3a+9k/5DFg+pTg==|7q1Arwz/ZfKEx+fksV3yo0HMQdypHJvyiix6hzgF3gY=|7lSXqjfq5rD3/3ofNZVpgv1ags696B2XXJryiGjDZvk=", $encKey, $macKey)
97165 => "https://example.com/login"
98166···103171(`|`) character, and then appended to the type (`2`) and a dot to form a
104172CipherString.
105173174174+ # encrypt+mac a value with a key and mac key and random iv, return cipherString
175175+ def encrypt(pt, key, macKey)
176176+ iv = OpenSSL::Random.random_bytes(16)
177177+178178+ cipher = OpenSSL::Cipher.new "AES-256-CBC"
179179+ cipher.encrypt
180180+ cipher.key = key
181181+ cipher.iv = iv
182182+ ct = cipher.update(pt)
183183+ ct << cipher.final
184184+185185+ mac = OpenSSL::HMAC.digest(OpenSSL::Digest.new("SHA256"), macKey, iv + ct)
186186+187187+ cipherString(2, Base64.strict_encode64(iv), Base64.strict_encode64(ct), Base64.strict_encode64(mac))
188188+ end
189189+106190 irb> encrypt("A secret note here...", $encKey, $macKey)
107191 => "2.NLkXMHtgR8u9azASR4XPOQ==|6/9QPcnoeQJDKBZTjcBAjVYJ7U/ArTch0hUSHZns6v8=|p55cl9FQK/Hef+7yzM7Cfe0w07q5hZI9tTbxupZepyM="
108192···192276A successful login will have a `200` status and a JSON response:
193277194278 {
195195- "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkJDMz[...](long random string)",
279279+ "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkJDMz[...](JWT string)",
196280 "expires_in": 3600,
197281 "token_type": "Bearer",
198282 "refresh_token": "28fb1911ef6db24025ce1bae5aa940e117eb09dfe609b425b69bff73d73c03bf",
···239323`TwoFactorToken` values are sent, but I'm not sure what these are for.
240324241325 {
242242- "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkJDMz[...](long string)",
326326+ "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkJDMz[...](JWT string)",
243327 "expires_in": 3600,
244328 "token_type": "Bearer",
245329 "refresh_token": "28fb1911ef6db24025ce1bae5aa940e117eb09dfe609b425b69bff73d73c03bf",
···250334251335The `access_token`, `refresh_token`, and `expires_in` values must be stored
252336and used for further API access.
337337+`$access_token` must be a
338338+[JWT](https://jwt.io/)
339339+string, which the browser extension decodes and parses, and must have at least
340340+`nbf`, `exp`, `iss`, `sub`, `email`, `name`, `premium`, and `iss` keys.
253341`$access_token` is sent as the `Authentication` header for up to `$expires_in`
254342seconds, after which the `$refresh_token` will need to be sent back to the
255343identity server to get a new `$access_token`.
···262350Issue a `GET` to `$baseURL/sync` with an `Authorization` header of the
263351`$access_token`.
264352265265- GET $baseURL(base url)/sync
353353+ GET $baseURL/sync
266354 Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkJDMz(rest of $access_token)
267355268356A successful response will contain a JSON body with `Profile`, `Folders`,