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
17require "rotp"
18
19class User < DBModel
20 self.table_name = "users"
21 #set_primary_key "uuid"
22
23 DEFAULT_KDF_TYPE = Bitwarden::KDF::PBKDF2
24
25 before_create :generate_uuid_primary_key
26 before_validation :generate_security_stamp
27
28 has_many :ciphers, foreign_key: :user_uuid, inverse_of: :user
29 has_many :folders, foreign_key: :user_uuid, inverse_of: :user
30 has_many :devices, foreign_key: :user_uuid, inverse_of: :user
31
32 def decrypt_data_with_master_password_key(data, mk)
33 # self.key is random data encrypted with the key of (password,email), so
34 # create that key and decrypt the random data to get the original
35 # encryption key, then use that key to decrypt the data
36 encKey = Bitwarden.decrypt(self.key, mk)
37 Bitwarden.decrypt(data, encKey)
38 end
39
40 def encrypt_data_with_master_password_key(data, mk)
41 # self.key is random data encrypted with the key of (password,email), so
42 # create that key and decrypt the random data to get the original
43 # encryption key, then use that key to encrypt the data
44 encKey = Bitwarden.decrypt(self.key, mk)
45 Bitwarden.encrypt(data, encKey)
46 end
47
48 def has_password_hash?(hash)
49 self.password_hash.timingsafe_equal_to(hash)
50 end
51
52 def to_hash
53 {
54 "Id" => self.uuid,
55 "Name" => self.name,
56 "Email" => self.email,
57 "EmailVerified" => self.email_verified,
58 "Premium" => self.premium,
59 "MasterPasswordHint" => self.password_hint,
60 "Culture" => self.culture,
61 "TwoFactorEnabled" => self.two_factor_enabled?,
62 "Key" => self.key,
63 "PrivateKey" => nil,
64 "SecurityStamp" => self.security_stamp,
65 "Organizations" => [],
66 "Object" => "profile"
67 }
68 end
69
70 def two_factor_enabled?
71 self.totp_secret.present?
72 end
73
74 def update_master_password(old_pwd, new_pwd,
75 new_kdf_iterations = self.kdf_iterations)
76 # original random encryption key must be preserved, just re-encrypted with
77 # a new key derived from the new password
78
79 orig_key = Bitwarden.decrypt(self.key,
80 Bitwarden.makeKey(old_pwd, self.email,
81 Bitwarden::KDF::TYPES[self.kdf_type], self.kdf_iterations))
82
83 self.key = Bitwarden.encrypt(orig_key,
84 Bitwarden.makeKey(new_pwd, self.email,
85 Bitwarden::KDF::TYPES[self.kdf_type], new_kdf_iterations)).to_s
86
87 self.password_hash = Bitwarden.hashPassword(new_pwd, self.email,
88 self.kdf_type, new_kdf_iterations)
89 self.kdf_iterations = new_kdf_iterations
90 self.security_stamp = SecureRandom.uuid
91 end
92
93 def verifies_totp_code?(code)
94 ROTP::TOTP.new(self.totp_secret).now == code.to_s
95 end
96
97protected
98 def generate_security_stamp
99 if self.security_stamp.blank?
100 self.security_stamp = SecureRandom.uuid
101 end
102 end
103end