An unofficial, mostly Bitwarden-compatible API server written in Ruby (Sinatra and ActiveRecord)
at master 345 lines 9.9 kB view raw
1# 2# Copyright (c) 2017 joshua stein <jcs@jcs.org> 3# 4# Permission to use, copy, modify, and distribute this software for any 5# purpose with or without fee is hereby granted, provided that the above 6# copyright notice and this permission notice appear in all copies. 7# 8# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15# 16 17module Rubywarden 18 module Routing 19 module Api 20 def self.registered(app) 21 app.namespace BASE_URL do 22 post "/accounts/prelogin" do 23 need_params(:email) do |p| 24 return validation_error("#{p} cannot be blank") 25 end 26 27 kdf_type = User::DEFAULT_KDF_TYPE 28 iterations = Bitwarden::KDF::DEFAULT_ITERATIONS[kdf_type] 29 30 if u = User.find_by_email(params[:email]) 31 iterations = u.kdf_iterations 32 kdf_type = Bitwarden::KDF::TYPES[u.kdf_type] 33 end 34 35 { 36 "Kdf" => Bitwarden::KDF::TYPE_IDS[kdf_type], 37 "KdfIterations" => iterations, 38 }.to_json 39 end 40 41 # create a new user 42 post "/accounts/register" do 43 content_type :json 44 45 if !ALLOW_SIGNUPS 46 return validation_error("Signups are not permitted") 47 end 48 49 need_params(:masterpasswordhash, :kdf, :kdfiterations) do |p| 50 return validation_error("#{p} cannot be blank") 51 end 52 53 if !params[:email].to_s.match(/^.+@.+\..+$/) 54 return validation_error("Invalid e-mail address") 55 end 56 57 kdf_type = Bitwarden::KDF::TYPES[params[:kdf].to_i] 58 if !kdf_type 59 return validation_error("invalid kdf type") 60 end 61 62 if !Bitwarden::KDF::ITERATION_RANGES[kdf_type]. 63 include?(params[:kdfiterations].to_i) 64 return validation_error("invalid kdf iterations") 65 end 66 67 begin 68 Bitwarden::CipherString.parse(params[:key]) 69 rescue Bitwarden::InvalidCipherString 70 return validation_error("Invalid key") 71 end 72 73 User.transaction do 74 params[:email].downcase! 75 76 if User.find_by_email(params[:email]) 77 return validation_error("E-mail is already in use") 78 end 79 80 u = User.new 81 u.email = params[:email] 82 u.password_hash = params[:masterpasswordhash] 83 u.password_hint = params[:masterpasswordhint] 84 u.key = params[:key] 85 u.kdf_type = Bitwarden::KDF::TYPE_IDS[kdf_type] 86 u.kdf_iterations = params[:kdfiterations] 87 88 # is this supposed to come from somewhere? 89 u.culture = "en-US" 90 91 # i am a fair and just god 92 u.premium = true 93 94 if !u.save 95 return validation_error("User save failed") 96 end 97 98 "" 99 end 100 end 101 102 # fetch profile and ciphers 103 get "/sync" do 104 d = device_from_bearer 105 if !d 106 return validation_error("invalid bearer") 107 end 108 109 { 110 "Profile" => d.user.to_hash, 111 "Folders" => d.user.folders.map{|f| f.to_hash }, 112 "Ciphers" => d.user.ciphers.map{|c| c.to_hash }, 113 "Domains" => { 114 "EquivalentDomains" => nil, 115 "GlobalEquivalentDomains" => [], 116 "Object" => "domains", 117 }, 118 "Object" => "sync", 119 }.to_json 120 end 121 122 # 123 # ciphers 124 # 125 126 # create a new cipher 127 post "/ciphers" do 128 d = device_from_bearer 129 if !d 130 return validation_error("invalid bearer") 131 end 132 133 need_params(:type, :name) do |p| 134 return validation_error("#{p} cannot be blank") 135 end 136 137 begin 138 Bitwarden::CipherString.parse(params[:name]) 139 rescue Bitwarden::InvalidCipherString 140 return validation_error("Invalid name") 141 end 142 143 if !params[:folderid].blank? 144 if !Folder.find_by_user_uuid_and_uuid(d.user_uuid, params[:folderid]) 145 return validation_error("Invalid folder") 146 end 147 end 148 149 c = Cipher.new 150 c.user_uuid = d.user_uuid 151 c.update_from_params(params) 152 153 Cipher.transaction do 154 if !c.save 155 return validation_error("error saving") 156 end 157 158 c.to_hash.merge({ 159 "Edit" => true, 160 }).to_json 161 end 162 end 163 164 # update a cipher 165 put "/ciphers/:uuid" do 166 d = device_from_bearer 167 if !d 168 return validation_error("invalid bearer") 169 end 170 171 c = nil 172 if params[:uuid].blank? || 173 !(c = Cipher.find_by_user_uuid_and_uuid(d.user_uuid, params[:uuid])) 174 return validation_error("invalid cipher") 175 end 176 177 need_params(:type, :name) do |p| 178 return validation_error("#{p} cannot be blank") 179 end 180 181 begin 182 Bitwarden::CipherString.parse(params[:name]) 183 rescue Bitwarden::InvalidCipherString 184 return validation_error("Invalid name") 185 end 186 187 if !params[:folderid].blank? 188 if !Folder.find_by_user_uuid_and_uuid(d.user_uuid, params[:folderid]) 189 return validation_error("Invalid folder") 190 end 191 end 192 193 c.update_from_params(params) 194 195 Cipher.transaction do 196 if !c.save 197 return validation_error("error saving") 198 end 199 200 c.to_hash.merge({ 201 "Edit" => true, 202 }).to_json 203 end 204 end 205 206 # delete a cipher 207 delete "/ciphers/:uuid" do 208 delete_cipher app: app, uuid: params[:uuid] 209 end 210 211 # delete a cipher (new client) 212 put "/ciphers/:uuid/delete" do 213 delete_cipher app: app, uuid: params[:uuid] 214 end 215 216 # 217 # folders 218 # 219 220 # create a new folder 221 post "/folders" do 222 d = device_from_bearer 223 if !d 224 return validation_error("invalid bearer") 225 end 226 227 need_params(:name) do |p| 228 return validation_error("#{p} cannot be blank") 229 end 230 231 begin 232 Bitwarden::CipherString.parse(params[:name]) 233 rescue 234 return validation_error("Invalid name") 235 end 236 237 f = Folder.new 238 f.user_uuid = d.user_uuid 239 f.update_from_params(params) 240 241 Folder.transaction do 242 if !f.save 243 return validation_error("error saving") 244 end 245 246 f.to_hash.to_json 247 end 248 end 249 250 # rename a folder 251 put "/folders/:uuid" do 252 d = device_from_bearer 253 if !d 254 return validation_error("invalid bearer") 255 end 256 257 f = nil 258 if params[:uuid].blank? || 259 !(f = Folder.find_by_user_uuid_and_uuid(d.user_uuid, params[:uuid])) 260 return validation_error("invalid folder") 261 end 262 263 need_params(:name) do |p| 264 return validation_error("#{p} cannot be blank") 265 end 266 267 begin 268 Bitwarden::CipherString.parse(params[:name]) 269 rescue 270 return validation_error("Invalid name") 271 end 272 273 f.update_from_params(params) 274 275 Folder.transaction do 276 if !f.save 277 return validation_error("error saving") 278 end 279 280 f.to_hash.to_json 281 end 282 end 283 284 # delete a folder 285 delete "/folders/:uuid" do 286 d = device_from_bearer 287 if !d 288 return validation_error("invalid bearer") 289 end 290 291 f = nil 292 if params[:uuid].blank? || 293 !(f = Folder.find_by_user_uuid_and_uuid(d.user_uuid, params[:uuid])) 294 return validation_error("invalid folder") 295 end 296 297 f.destroy 298 299 "" 300 end 301 302 # 303 # device push tokens 304 # 305 306 put "/devices/identifier/:uuid/clear-token" do 307 # XXX: for some reason, the iOS app doesn't send an Authorization header 308 # for this 309 d = device_from_bearer 310 if !d 311 return validation_error("invalid bearer") 312 end 313 314 d.push_token = nil 315 316 Device.transaction do 317 if !d.save 318 return validation_error("error saving") 319 end 320 321 "" 322 end 323 end 324 325 put "/devices/identifier/:uuid/token" do 326 d = device_from_bearer 327 if !d 328 return validation_error("invalid bearer") 329 end 330 331 d.push_token = params[:pushtoken] 332 333 Device.transaction do 334 if !d.save 335 return validation_error("error saving") 336 end 337 338 "" 339 end 340 end 341 end 342 end 343 end 344 end 345end