An unofficial, mostly Bitwarden-compatible API server written in Ruby (Sinatra and ActiveRecord)
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