An unofficial, mostly Bitwarden-compatible API server written in Ruby (Sinatra and ActiveRecord)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement HKDF key stretching for Bitwarden.{en,de}crypt

Pass the full key to encrypt and decrypt, along with the algorithm
(now defaulting to TYPE_AESCBC256_HMACSHA256_B64). If the algorithm
is TYPE_AESCBC256_HMACSHA256_B64 but the key is only 32 bytes, use
HKDF to stretch it and separate it into encryption and MAC keys.

Ref: https://github.com/bitwarden/jslib/commit/0429c0557b293ca97ea684ad8bb500c036d88ae3

Ref #79

+102 -47
+75 -24
lib/bitwarden.rb
··· 51 51 PBKDF2.new(:password => password, :salt => salt, 52 52 :iterations => kdf_iterations, 53 53 :hash_function => OpenSSL::Digest::SHA256, 54 - :key_length => (256 / 8)).bin_string 54 + :key_length => 32).bin_string 55 55 else 56 56 raise "unknown kdf type #{kdf_type.inspect}" 57 57 end 58 58 end 59 59 60 - # encrypt random bytes with a key to make new encryption key 61 - def makeEncKey(key) 60 + # encrypt random bytes with a key from makeKey to make a new encryption 61 + # CipherString 62 + def makeEncKey(key, algo = CipherString::TYPE_AESCBC256_HMACSHA256_B64) 62 63 pt = OpenSSL::Random.random_bytes(64) 63 - iv = OpenSSL::Random.random_bytes(16) 64 - 65 - cipher = OpenSSL::Cipher.new "AES-256-CBC" 66 - cipher.encrypt 67 - cipher.key = key 68 - cipher.iv = iv 69 - ct = cipher.update(pt) 70 - ct << cipher.final 71 - 72 - CipherString.new( 73 - CipherString::TYPE_AESCBC256_B64, 74 - Base64.strict_encode64(iv), 75 - Base64.strict_encode64(ct), 76 - ).to_s 64 + encrypt(pt, key, algo).to_s 77 65 end 78 66 79 67 # base64-encode a wrapped, stretched password+salt for signup/login ··· 93 81 94 82 # encrypt+mac a value with a key and mac key and random iv, return a 95 83 # CipherString of it 96 - def encrypt(pt, key, macKey = nil) 84 + def encrypt(pt, key, algo = CipherString::TYPE_AESCBC256_HMACSHA256_B64) 85 + mac = nil 86 + macKey = nil 87 + 88 + case algo 89 + when CipherString::TYPE_AESCBC256_B64 90 + if key.bytesize != 32 91 + raise "unhandled key size #{key.bytesize}" 92 + end 93 + 94 + when CipherString::TYPE_AESCBC256_HMACSHA256_B64 95 + macKey = nil 96 + if key.bytesize == 32 97 + tkey = hkdfStretch(key, "enc", 32) 98 + macKey = hkdfStretch(key, "mac", 32) 99 + key = tkey 100 + elsif key.bytesize == 64 101 + macKey = key[32, 32] 102 + key = key[0, 32] 103 + else 104 + raise "invalid key size #{key.bytesize}" 105 + end 106 + else 107 + raise "TODO: #{algo}" 108 + end 109 + 97 110 iv = OpenSSL::Random.random_bytes(16) 98 111 99 112 cipher = OpenSSL::Cipher.new "AES-256-CBC" ··· 127 140 end 128 141 129 142 # decrypt a CipherString and return plaintext 130 - def decrypt(str, key, macKey) 131 - c = CipherString.parse(str) 132 - iv = Base64.decode64(c.iv) 133 - ct = Base64.decode64(c.ct) 134 - mac = c.mac ? Base64.decode64(c.mac) : nil 143 + def decrypt(cs, key) 144 + if !cs.is_a?(CipherString) 145 + cs = CipherString.parse(cs) 146 + end 147 + 148 + iv = Base64.decode64(cs.iv) 149 + ct = Base64.decode64(cs.ct) 150 + mac = cs.mac ? Base64.decode64(cs.mac) : nil 135 151 136 - case c.type 152 + case cs.type 137 153 when CipherString::TYPE_AESCBC256_B64 138 154 cipher = OpenSSL::Cipher.new "AES-256-CBC" 139 155 cipher.decrypt ··· 144 160 return pt 145 161 146 162 when CipherString::TYPE_AESCBC256_HMACSHA256_B64 163 + macKey = nil 164 + if key.bytesize == 32 165 + tkey = hkdfStretch(key, "enc", 32) 166 + macKey = hkdfStretch(key, "mac", 32) 167 + key = tkey 168 + elsif key.bytesize == 64 169 + macKey = key[32, 32] 170 + key = key[0, 32] 171 + else 172 + raise "invalid key size #{key.bytesize}" 173 + end 174 + 147 175 cmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new("SHA256"), 148 176 macKey, iv + ct) 149 177 if !self.macsEqual(macKey, mac, cmac) ··· 161 189 else 162 190 raise "TODO implement #{c.type}" 163 191 end 192 + end 193 + 194 + def hkdfStretch(prk, info, size) 195 + hashlen = 32 196 + prev = [] 197 + okm = [] 198 + n = (size / hashlen.to_f).ceil 199 + n.times do |x| 200 + t = [] 201 + t += prev 202 + t += info.split("").map{|c| c.ord } 203 + t += [ (x + 1) ] 204 + hmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new("SHA256"), prk, 205 + t.map{|c| c.chr }.join("")) 206 + prev = hmac.bytes 207 + okm += hmac.bytes 208 + end 209 + 210 + if okm.length != size 211 + raise "invalid hkdf result: #{okm.length} != #{size}" 212 + end 213 + 214 + okm.map{|c| c.chr }.join("") 164 215 end 165 216 end 166 217
+6 -5
lib/user.rb
··· 33 33 # self.key is random data encrypted with the key of (password,email), so 34 34 # create that key and decrypt the random data to get the original 35 35 # encryption key, then use that key to decrypt the data 36 - encKey = Bitwarden.decrypt(self.key, mk[0, 32], mk[32, 32]) 37 - Bitwarden.decrypt(data, encKey[0, 32], encKey[32, 32]) 36 + encKey = Bitwarden.decrypt(self.key, mk) 37 + Bitwarden.decrypt(data, encKey) 38 38 end 39 39 40 40 def encrypt_data_with_master_password_key(data, mk) 41 41 # self.key is random data encrypted with the key of (password,email), so 42 42 # create that key and decrypt the random data to get the original 43 43 # encryption key, then use that key to encrypt the data 44 - encKey = Bitwarden.decrypt(self.key, mk[0, 32], mk[32, 32]) 45 - Bitwarden.encrypt(data, encKey[0, 32], encKey[32, 32]) 44 + encKey = Bitwarden.decrypt(self.key, mk) 45 + Bitwarden.encrypt(data, encKey) 46 46 end 47 47 48 48 def has_password_hash?(hash) ··· 77 77 78 78 orig_key = Bitwarden.decrypt(self.key, 79 79 Bitwarden.makeKey(old_pwd, self.email, 80 - Bitwarden::KDF::TYPES[self.kdf_type], self.kdf_iterations), nil) 80 + Bitwarden::KDF::TYPES[self.kdf_type], self.kdf_iterations)) 81 81 82 82 self.key = Bitwarden.encrypt(orig_key, 83 83 Bitwarden.makeKey(new_pwd, self.email, ··· 91 91 def verifies_totp_code?(code) 92 92 ROTP::TOTP.new(self.totp_secret).now == code.to_s 93 93 end 94 + 94 95 protected 95 96 def generate_security_stamp 96 97 if self.security_stamp.blank?
+3 -2
spec/cipher_spec.rb
··· 94 94 ik = Bitwarden.makeKey("asdf", "api@example.com", 95 95 User::DEFAULT_KDF_TYPE, 96 96 Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]) 97 - k = Bitwarden.makeEncKey(ik) 98 - new_name = Bitwarden.encrypt("some new name", k[0, 32], k[32, 32]).to_s 97 + ek = Bitwarden.makeEncKey(ik) 98 + k = Bitwarden.decrypt(ek, ik) 99 + new_name = Bitwarden.encrypt("some new name", k).to_s 99 100 100 101 put_json "/api/ciphers/#{uuid}", { 101 102 :type => 1,
+17 -14
spec/cipherstring_spec.rb
··· 19 19 end 20 20 21 21 it "should make a cipher string from a key" do 22 - cs = Bitwarden.makeEncKey( 23 - Bitwarden.makeKey("this is a password", "nobody@example.com", 24 - Bitwarden::KDF::PBKDF2, 5000) 25 - ) 22 + cs = Bitwarden.makeEncKey(Bitwarden.makeKey("this is a password", 23 + "nobody@example.com", Bitwarden::KDF::PBKDF2, 5000), 24 + Bitwarden::CipherString::TYPE_AESCBC256_B64) 26 25 27 26 cs.must_match(/^0\.[^|]+|[^|]+$/) 27 + 28 + cs = Bitwarden.makeEncKey(Bitwarden.makeKey("this is a password", 29 + "nobody@example.com", Bitwarden::KDF::PBKDF2, 5000), 30 + Bitwarden::CipherString::TYPE_AESCBC256_HMACSHA256_B64) 31 + 32 + cs.must_match(/^2\.[^|]+|[^|]+$/) 28 33 end 29 34 30 35 it "should hash a password" do 31 - #def hashedPassword(password, salt) 32 36 Bitwarden.hashPassword("secret password", "user@example.com", 33 37 Bitwarden::KDF::PBKDF2, 5000).must_equal "VRlYxg0x41v40mvDNHljqpHcqlIFwQSzegeq+POW1ww=" 34 38 end ··· 44 48 cs.mac.must_be_nil 45 49 end 46 50 47 - it "should parse a type-3 cipher string" do 51 + it "should parse a type-2 cipher string" do 48 52 cs = Bitwarden::CipherString.parse("2.ftF0nH3fGtuqVckLZuHGjg==|u0VRhH24uUlVlTZd/uD1lA==|XhBhBGe7or/bXzJRFWLUkFYqauUgxksCrRzNmJyigfw=") 49 53 cs.type.must_equal 2 50 54 end 51 55 52 56 it "should encrypt and decrypt properly" do 53 - ik = Bitwarden.makeKey("password", "user@example.com", 57 + mk = Bitwarden.makeKey("password", "user@example.com", 54 58 Bitwarden::KDF::PBKDF2, 5000) 55 - ek = Bitwarden.makeEncKey(ik) 56 - k = Bitwarden.decrypt(ek, ik, nil) 57 - j = Bitwarden.encrypt("hi there", k[0, 32], k[32, 32]) 59 + ek = Bitwarden.makeEncKey(mk) 60 + k = Bitwarden.decrypt(ek, mk) 61 + j = Bitwarden.encrypt("hi there", k) 58 62 59 - cs = Bitwarden::CipherString.parse(j) 60 - 61 - ik = Bitwarden.makeKey("password", "user@example.com", 63 + mk = Bitwarden.makeKey("password", "user@example.com", 62 64 Bitwarden::KDF::PBKDF2, 5000) 63 - Bitwarden.decrypt(cs.to_s, k[0, 32], k[32, 32]).must_equal "hi there" 65 + k = Bitwarden.decrypt(ek, mk) 66 + Bitwarden.decrypt(j, k).must_equal "hi there" 64 67 end 65 68 66 69 it "should test mac equality" do
+1 -2
spec/folder_spec.rb
··· 75 75 ik = Bitwarden.makeKey("asdf", "api@example.com", 76 76 User::DEFAULT_KDF_TYPE, 77 77 Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]) 78 - k = Bitwarden.makeEncKey(ik) 79 - new_name = Bitwarden.encrypt("some new name", k[0, 32], k[32, 32]).to_s 78 + new_name = Bitwarden.encrypt("some new name", ik).to_s 80 79 81 80 put_json "/api/folders/#{uuid}", { 82 81 :name => new_name,