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