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.

Integrate ActiveRecord (#44)


authored by

Johannes / universa1 and committed by
joshua stein
5ab123d4 0f3da81a

+414 -544
+11 -2
Gemfile
··· 4 4 5 5 gem "sinatra", "~> 2.0.3" 6 6 gem "sinatra-contrib", "~> 2.0.3" 7 + 8 + gem "activerecord", "~> 5.1.5" 9 + gem "sinatra-activerecord", "~> 2.0.13" 10 + gem "sqlite3" 11 + 7 12 gem "unicorn" 8 13 gem "json" 9 14 10 15 gem "pbkdf2-ruby" 11 16 gem "rotp" 12 17 gem "jwt" 13 - 14 - gem "sqlite3" 15 18 16 19 # for tools/activate_totp.rb 17 20 gem "rqrcode" ··· 24 27 group :keepass, :optional => true do 25 28 gem 'rubeepass', '~> 3.0' 26 29 end 30 + 31 + group :migrate, optional: true do 32 + gem 'yaml_db' 33 + end 34 + 35 + gem 'pry'
+117 -20
Gemfile.lock
··· 1 1 GEM 2 2 remote: https://rubygems.org/ 3 3 specs: 4 - activesupport (5.2.0) 4 + actioncable (5.1.6) 5 + actionpack (= 5.1.6) 6 + nio4r (~> 2.0) 7 + websocket-driver (~> 0.6.1) 8 + actionmailer (5.1.6) 9 + actionpack (= 5.1.6) 10 + actionview (= 5.1.6) 11 + activejob (= 5.1.6) 12 + mail (~> 2.5, >= 2.5.4) 13 + rails-dom-testing (~> 2.0) 14 + actionpack (5.1.6) 15 + actionview (= 5.1.6) 16 + activesupport (= 5.1.6) 17 + rack (~> 2.0) 18 + rack-test (>= 0.6.3) 19 + rails-dom-testing (~> 2.0) 20 + rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 + actionview (5.1.6) 22 + activesupport (= 5.1.6) 23 + builder (~> 3.1) 24 + erubi (~> 1.4) 25 + rails-dom-testing (~> 2.0) 26 + rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 + activejob (5.1.6) 28 + activesupport (= 5.1.6) 29 + globalid (>= 0.3.6) 30 + activemodel (5.1.6) 31 + activesupport (= 5.1.6) 32 + activerecord (5.1.6) 33 + activemodel (= 5.1.6) 34 + activesupport (= 5.1.6) 35 + arel (~> 8.0) 36 + activesupport (5.1.6) 5 37 concurrent-ruby (~> 1.0, >= 1.0.2) 6 38 i18n (>= 0.7, < 2) 7 39 minitest (~> 5.1) 8 40 tzinfo (~> 1.1) 41 + arel (8.0.0) 9 42 backports (3.11.3) 10 - chunky_png (1.3.8) 43 + builder (3.2.3) 44 + chunky_png (1.3.10) 45 + coderay (1.1.2) 11 46 concurrent-ruby (1.0.5) 12 - djinni (2.2.0) 13 - fagin (~> 1.0, >= 1.0.3) 14 - fagin (1.1.2) 15 - hilighter (1.2.2) 47 + crass (1.0.4) 48 + djinni (2.2.4) 49 + fagin (~> 1.2, >= 1.2.1) 50 + erubi (1.7.1) 51 + fagin (1.2.1) 52 + globalid (0.4.1) 53 + activesupport (>= 4.2.0) 54 + hilighter (1.2.3) 16 55 i18n (1.0.1) 17 56 concurrent-ruby (~> 1.0) 18 57 json (2.1.0) 19 - json_config (0.1.3) 58 + json_config (0.1.4) 20 59 jwt (2.1.0) 21 - kgio (2.11.0) 22 - minitest (5.10.3) 60 + kgio (2.11.2) 61 + loofah (2.2.2) 62 + crass (~> 1.0.2) 63 + nokogiri (>= 1.5.9) 64 + mail (2.7.0) 65 + mini_mime (>= 0.1.1) 66 + method_source (0.9.0) 67 + mini_mime (1.0.0) 68 + mini_portile2 (2.3.0) 69 + minitest (5.11.3) 23 70 multi_json (1.13.1) 24 71 mustermann (1.0.2) 72 + nio4r (2.3.1) 73 + nokogiri (1.8.4) 74 + mini_portile2 (~> 2.3.0) 25 75 os (1.0.0) 26 76 pbkdf2-ruby (0.2.1) 77 + pry (0.11.3) 78 + coderay (~> 1.1.0) 79 + method_source (~> 0.9.0) 27 80 rack (2.0.5) 28 81 rack-protection (2.0.3) 29 82 rack 30 - rack-test (0.7.0) 83 + rack-test (1.1.0) 31 84 rack (>= 1.0, < 3) 85 + rails (5.1.6) 86 + actioncable (= 5.1.6) 87 + actionmailer (= 5.1.6) 88 + actionpack (= 5.1.6) 89 + actionview (= 5.1.6) 90 + activejob (= 5.1.6) 91 + activemodel (= 5.1.6) 92 + activerecord (= 5.1.6) 93 + activesupport (= 5.1.6) 94 + bundler (>= 1.3.0) 95 + railties (= 5.1.6) 96 + sprockets-rails (>= 2.0.0) 97 + rails-dom-testing (2.0.3) 98 + activesupport (>= 4.2.0) 99 + nokogiri (>= 1.6) 100 + rails-html-sanitizer (1.0.4) 101 + loofah (~> 2.2, >= 2.2.2) 102 + railties (5.1.6) 103 + actionpack (= 5.1.6) 104 + activesupport (= 5.1.6) 105 + method_source 106 + rake (>= 0.8.7) 107 + thor (>= 0.18.1, < 2.0) 32 108 raindrops (0.19.0) 33 - rake (12.3.0) 34 - rotp (3.3.0) 109 + rake (12.3.1) 110 + rotp (3.3.1) 35 111 rqrcode (0.10.1) 36 112 chunky_png (~> 1.0) 37 - rubeepass (3.0.0) 38 - djinni (~> 2.2, >= 2.2.0) 39 - hilighter (~> 1.1, >= 1.1.1) 40 - json_config (~> 0.1, >= 0.1.3) 113 + rubeepass (3.1.0) 114 + djinni (~> 2.2, >= 2.2.4) 115 + hilighter (~> 1.1, >= 1.2.3) 116 + json_config (~> 0.1, >= 0.1.4) 41 117 os (~> 1.0, >= 1.0.0) 42 118 salsa20 (~> 0.1, >= 0.1.2) 43 - scoobydoo (~> 0.1, >= 0.1.5) 119 + scoobydoo (~> 0.1, >= 0.1.6) 44 120 salsa20 (0.1.2) 45 - scoobydoo (0.1.5) 121 + scoobydoo (0.1.6) 46 122 sinatra (2.0.3) 47 123 mustermann (~> 1.0) 48 124 rack (~> 2.0) 49 125 rack-protection (= 2.0.3) 50 126 tilt (~> 2.0) 127 + sinatra-activerecord (2.0.13) 128 + activerecord (>= 3.2) 129 + sinatra (>= 1.0) 51 130 sinatra-contrib (2.0.3) 52 131 activesupport (>= 4.0.0) 53 132 backports (>= 2.8.2) ··· 56 135 rack-protection (= 2.0.3) 57 136 sinatra (= 2.0.3) 58 137 tilt (>= 1.3, < 3) 138 + sprockets (3.7.2) 139 + concurrent-ruby (~> 1.0) 140 + rack (> 1, < 3) 141 + sprockets-rails (3.2.1) 142 + actionpack (>= 4.0) 143 + activesupport (>= 4.0) 144 + sprockets (>= 3.0.0) 59 145 sqlite3 (1.3.13) 146 + thor (0.20.0) 60 147 thread_safe (0.3.6) 61 148 tilt (2.0.8) 62 149 tzinfo (1.2.5) 63 150 thread_safe (~> 0.1) 64 - unicorn (5.3.1) 151 + unicorn (5.4.1) 65 152 kgio (~> 2.6) 66 153 raindrops (~> 0.7) 154 + websocket-driver (0.6.5) 155 + websocket-extensions (>= 0.1.0) 156 + websocket-extensions (0.1.3) 157 + yaml_db (0.7.0) 158 + rails (>= 3.0) 159 + rake (>= 0.8.7) 67 160 68 161 PLATFORMS 69 162 ruby 70 163 71 164 DEPENDENCIES 165 + activerecord (~> 5.1.5) 72 166 json 73 167 jwt 74 168 minitest 75 169 pbkdf2-ruby 170 + pry 76 171 rack-test 77 172 rake 78 173 rotp 79 174 rqrcode 80 175 rubeepass (~> 3.0) 81 176 sinatra (~> 2.0.3) 177 + sinatra-activerecord (~> 2.0.13) 82 178 sinatra-contrib (~> 2.0.3) 83 179 sqlite3 84 180 unicorn 181 + yaml_db 85 182 86 183 RUBY VERSION 87 184 ruby 2.4.2p198 88 185 89 186 BUNDLED WITH 90 - 1.16.0 187 + 1.16.1
+31 -1
README.md
··· 37 37 38 38 Run `bundle install` at least once. 39 39 40 + In order to create the database and the required tables run: 41 + 42 + env RACK_ENV=production bundle exec rake db:migrate 43 + 40 44 To run via Rack on port 4567: 41 45 42 46 env RACK_ENV=production bundle exec rackup -p 4567 config.ru ··· 63 67 ``` 64 68 env RACK_ENV=production bundle exec ruby tools/change_master_password.rb -u you@example.com 65 69 ``` 70 + 71 + ### Migrating to ActiveRecord 72 + 73 + If you've used this library before it switched to using ActiveRecord, you need to do the following steps to migrate the data and generate the new table structures. Even though the migration script will create a backup of your database, it is probably best to create a backup yourself. You can also copy the ```db/production.sqlite3``` to your local machine and do the migration there. After a successful migration you'd have to copy the updated database file back to the production machine. 74 + 75 + First make sure you have the latest code: 76 + 77 + git pull 78 + 79 + Afterwards you need to run bundle to add some required libraries for the migration 80 + 81 + bundle --with migrate 82 + 83 + Now you are ready to do the migration 84 + 85 + ruby tools/migrate_to_ar.rb -e production 86 + 87 + The -e switch allows you to select the correct database environment from db/config.yml. The migration script will:# 88 + 89 + * dump the contents of the database to a YAML file 90 + * rename the original database file to ```production.sqlite3.#{Time.now.to_i}``` 91 + * create the database using ActiveRecord migrations 92 + * load the contents from the dump file 93 + * remove the dump file 94 + 95 + Now your data is completely migrated and the library will now use ActiveRecord to handle anything database related :-) 66 96 67 97 ### 1Password Conversion 68 98 ··· 127 157 128 158 ### Keepass Conversion 129 159 130 - In order to use the Keepass converter, you will need to install the necessary 160 + In order to use the Keepass converter, you will need to install the necessary 131 161 dependency, using `bundle install --with keepass`. 132 162 133 163 There is no need to export your Keepass-database - you can use it as is.
+7
Rakefile
··· 1 1 require "rake/testtask" 2 + require "sinatra/activerecord/rake" 2 3 3 4 Rake::TestTask.new do |t| 4 5 t.pattern = "spec/*_spec.rb" 5 6 end 7 + 8 + namespace :db do 9 + task :load_config do 10 + require "./app" 11 + end 12 + end
+17
db/config.yml
··· 1 + development: 2 + adapter: sqlite3 3 + database: db/development.sqlite3 4 + pool: 5 5 + timeout: 5000 6 + 7 + test: 8 + adapter: sqlite3 9 + database: ":memory:" 10 + pool: 5 11 + timeout: 5000 12 + 13 + production: 14 + adapter: sqlite3 15 + database: db/production.sqlite3 16 + pool: 5 17 + timeout: 5000
+20
db/migrate/20180324145941_create_users.rb
··· 1 + class CreateUsers < ActiveRecord::Migration[5.1] 2 + def change 3 + create_table :users, id: :string, primary_key: :uuid do |t| 4 + t.text :email 5 + t.boolean :email_verified, default: true 6 + t.boolean :premium, default: true 7 + t.text :name 8 + t.text :password_hash 9 + t.text :password_hint 10 + t.text :key 11 + t.binary :private_key 12 + t.binary :public_key 13 + t.string :totp_secret 14 + t.string :security_stamp 15 + t.string :culture 16 + t.timestamps 17 + t.index :email, unique: true 18 + end 19 + end 20 + end
+20
db/migrate/20180324151103_create_devices.rb
··· 1 + class CreateDevices < ActiveRecord::Migration[5.1] 2 + def change 3 + create_table :devices, id: :string, primary_key: :uuid do |t| 4 + t.string :user_uuid 5 + t.string :name 6 + t.integer :type 7 + t.string :push_token 8 + t.string :access_token 9 + t.string :refresh_token 10 + t.datetime :token_expires_at 11 + t.timestamps 12 + t.index :push_token, unique: true 13 + t.index :access_token, unique: true 14 + t.index :refresh_token, unique: true 15 + end 16 + add_foreign_key :devices, :users, { column: :user_uuid, primary_key: :uuid } 17 + add_index(:devices, :user_uuid) 18 + 19 + end 20 + end
+12
db/migrate/20180324151113_create_folders.rb
··· 1 + class CreateFolders < ActiveRecord::Migration[5.1] 2 + def change 3 + create_table :folders, id: :string, primary_key: :uuid do |t| 4 + t.string :user_uuid 5 + t.binary :name 6 + t.timestamps 7 + end 8 + add_foreign_key :folders, :users, { column: :user_uuid, primary_key: :uuid } 9 + add_index(:folders, :user_uuid) 10 + 11 + end 12 + end
+26
db/migrate/20180324151117_create_ciphers.rb
··· 1 + class CreateCiphers < ActiveRecord::Migration[5.1] 2 + def change 3 + create_table :ciphers, id: :string, primary_key: :uuid do |t| 4 + t.string :user_uuid 5 + t.string :folder_uuid 6 + t.string :organization_uuid 7 + t.integer :type 8 + t.binary :data 9 + t.boolean :favorite 10 + t.binary :attachments 11 + t.binary :name 12 + t.binary :notes 13 + t.binary :fields 14 + t.binary :login 15 + t.binary :card 16 + t.binary :identity 17 + t.binary :securenote 18 + t.timestamps 19 + end 20 + add_foreign_key :ciphers, :users, { column: :user_uuid, primary_key: :uuid } 21 + add_index(:ciphers, :user_uuid) 22 + add_foreign_key :ciphers, :folders, { column: :folder_uuid, primary_key: :uuid } 23 + add_index(:ciphers, :folder_uuid) 24 + 25 + end 26 + end
+7
db/migrate/20180518070354_set_default_value_for_favorite.rb
··· 1 + class SetDefaultValueForFavorite < ActiveRecord::Migration[5.1] 2 + class Cipher < ActiveRecord::Base; end 3 + def change 4 + change_column_default :ciphers, :favorite, false 5 + change_column_null(:ciphers, :favorite, false, false) 6 + end 7 + end
+5
lib/app.rb
··· 14 14 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 15 # 16 16 17 + require 'sinatra/activerecord' 18 + 17 19 require_relative 'helpers/request_helpers' 18 20 19 21 require_relative 'routes/api' ··· 23 25 module BitwardenRuby 24 26 class App < Sinatra::Base 25 27 register Sinatra::Namespace 28 + register Sinatra::ActiveRecordExtension 26 29 27 30 set :root, File.dirname(__FILE__) 31 + set :database_file, "../db/config.yml" 32 + 28 33 configure do 29 34 enable :logging 30 35 end
+5 -2
lib/bitwarden_ruby.rb
··· 20 20 21 21 RACK_ENV ||= (ENV["RACK_ENV"] || "development") 22 22 23 + require "sqlite3" 24 + require "active_record" 25 + 23 26 require "sinatra/base" 24 27 require "sinatra/namespace" 25 28 require "cgi" ··· 46 49 # create/load JWT signing keys 47 50 Bitwarden::Token.load_keys 48 51 49 - # create/update tables 50 - Db.connect("#{APP_ROOT}/db/#{RACK_ENV}.sqlite3") 52 + # connect to db 53 + Db.connect(environment: RACK_ENV)
+25 -30
lib/cipher.rb
··· 15 15 # 16 16 17 17 class Cipher < DBModel 18 - set_table_name "ciphers" 19 - set_primary_key "uuid" 18 + self.table_name = "ciphers" 19 + #set_primary_key "uuid" 20 20 21 - attr_writer :user 21 + before_create :generate_uuid_primary_key 22 + 23 + belongs_to :user, foreign_key: :user_uuid, inverse_of: :folders 24 + belongs_to :folder, foreign_key: :folder_uuid, inverse_of: :ciphers, optional: true 25 + 26 + serialize :fields, JSON 27 + serialize :login, JSON 28 + serialize :securenote, JSON 29 + serialize :card, JSON 30 + serialize :identity, JSON 31 + serialize :attachments, JSON 22 32 23 33 TYPE_LOGIN = 1 24 34 TYPE_NOTE = 2 ··· 40 50 end 41 51 end 42 52 43 - # shortcut to turn any field containing json data into an object 44 - def method_missing(method, *args, &block) 45 - if m = method.to_s.match(/^(.+)_unjson$/) 46 - j = self.send(m[1]) 47 - j ? JSON.parse(j) : nil 48 - else 49 - super 50 - end 51 - end 52 - 53 53 # migrate from older style everything-in-data to separate fields 54 54 def migrate_data! 55 55 return if !self.data ··· 59 59 60 60 self.name = js.delete("Name") 61 61 self.notes = js.delete("Notes") 62 - f = js.delete("Fields") 63 - self.fields = f ? f.to_json : nil 62 + self.fields = js.delete("Fields") 64 63 65 64 if self.type == TYPE_LOGIN 66 65 js["Uris"] = [ ··· 76 75 TYPE_CARD => "card", 77 76 TYPE_IDENTITY => "identity", 78 77 } 79 - self.send("#{fmap[self.type]}=", js.to_json) 78 + self.send("#{fmap[self.type]}=", js) 80 79 81 80 self.save || raise("failed migrating #{self.inspect}") 82 81 end ··· 94 93 "Object" => "cipher", 95 94 "Name" => self.name, 96 95 "Notes" => self.notes, 97 - "Fields" => self.fields_unjson, 98 - "Login" => self.login_unjson, 99 - "Card" => self.card_unjson, 100 - "Identity" => self.identity_unjson, 101 - "SecureNote" => self.securenote_unjson, 96 + "Fields" => self.fields, 97 + "Login" => self.login, 98 + "Card" => self.card, 99 + "Identity" => self.identity, 100 + "SecureNote" => self.securenote, 102 101 } 103 102 end 104 103 ··· 113 112 114 113 self.fields = nil 115 114 if params[:fields] && params[:fields].is_a?(Array) 116 - self.fields = params[:fields].map{|h| h.ucfirst_hash }.to_json 115 + self.fields = params[:fields].map{|h| h.ucfirst_hash } 117 116 end 118 117 119 118 case self.type ··· 124 123 tlogin["Uris"].map!{|h| h.ucfirst_hash } 125 124 end 126 125 127 - self.login = tlogin.to_json 126 + self.login = tlogin 128 127 129 128 when TYPE_NOTE 130 - self.securenote = params[:securenote].ucfirst_hash.to_json 129 + self.securenote = params[:securenote].ucfirst_hash 131 130 132 131 when TYPE_CARD 133 - self.card = params[:card].ucfirst_hash.to_json 132 + self.card = params[:card].ucfirst_hash 134 133 135 134 when TYPE_IDENTITY 136 - self.identity = params[:identity].ucfirst_hash.to_json 135 + self.identity = params[:identity].ucfirst_hash 137 136 end 138 - end 139 - 140 - def user 141 - @user ||= User.find_by_uuid(self.user_uuid) 142 137 end 143 138 end
+4 -171
lib/db.rb
··· 16 16 17 17 # 18 18 # To make a db change: 19 - # - modify the schema in #connect for a new install 20 - # - bump DB_VERSION 21 - # - add a case in #migrate_from to migrate up to that new version 19 + # - generate a new migration using rake db:new_migration name=YOUR_NAME 22 20 # 23 - 24 - require "sqlite3" 25 - 26 21 class Db 27 - class << self 28 - DB_VERSION = 3 29 - 30 - attr_reader :db, :db_file 31 - 32 - def connect(db_file) 33 - @db_file = db_file 34 - 35 - @db = SQLite3::Database.new(@db_file) 36 - 37 - @db.execute(" 38 - CREATE TABLE IF NOT EXISTS 39 - users 40 - (uuid STRING PRIMARY KEY, 41 - created_at DATETIME, 42 - updated_at DATETIME, 43 - email TEXT UNIQUE, 44 - email_verified BOOLEAN, 45 - premium BOOLEAN, 46 - name TEXT, 47 - password_hash TEXT, 48 - password_hint TEXT, 49 - key TEXT, 50 - private_key BLOB, 51 - public_key BLOB, 52 - totp_secret STRING, 53 - security_stamp STRING, 54 - culture STRING) 55 - ") 56 - 57 - @db.execute(" 58 - CREATE TABLE IF NOT EXISTS 59 - devices 60 - (uuid STRING PRIMARY KEY, 61 - created_at DATETIME, 62 - updated_at DATETIME, 63 - user_uuid STRING, 64 - name STRING, 65 - type INTEGER, 66 - push_token STRING UNIQUE, 67 - access_token STRING UNIQUE, 68 - refresh_token STRING UNIQUE, 69 - token_expires_at DATETIME) 70 - ") 71 - 72 - @db.execute(" 73 - CREATE TABLE IF NOT EXISTS 74 - ciphers 75 - (uuid STRING PRIMARY KEY, 76 - created_at DATETIME, 77 - updated_at DATETIME, 78 - user_uuid STRING, 79 - folder_uuid STRING, 80 - organization_uuid STRING, 81 - type INTEGER, 82 - data BLOB, 83 - favorite BOOLEAN, 84 - attachments BLOB, 85 - name BLOB, 86 - notes BLOB, 87 - fields BLOB, 88 - login BLOB, 89 - card BLOB, 90 - identity BLOB, 91 - securenote BLOB) 92 - ") 93 - 94 - @db.execute(" 95 - CREATE TABLE IF NOT EXISTS 96 - folders 97 - (uuid STRING PRIMARY KEY, 98 - created_at DATETIME, 99 - updated_at DATETIME, 100 - user_uuid STRING, 101 - name BLOB) 102 - ") 103 - 104 - @db.results_as_hash = true 105 - 106 - @db.execute(" 107 - CREATE TABLE IF NOT EXISTS 108 - schema_version 109 - (version INTEGER) 110 - ") 111 - 112 - last_version = 0 113 - while true 114 - v = @db.execute("SELECT version FROM schema_version").first 115 - if v 116 - if v["version"] == last_version 117 - raise "looping in migrations, #{last_version} didn't increment" 118 - elsif v["version"] == DB_VERSION 119 - break 120 - end 121 - else 122 - v = { "version" => 0 } 123 - end 124 - 125 - last_version = v["version"] 126 - migrate_from(v["version"]) 127 - end 128 - 129 - # eagerly cache column definitions 130 - ObjectSpace.each_object(Class).each do |klass| 131 - if klass < DBModel 132 - klass.fetch_columns 133 - end 134 - end 135 - 136 - @db 137 - end 138 - 139 - def connection 140 - @db 141 - end 142 - 143 - def execute(query, params = []) 144 - # debug point: 145 - # STDERR.puts(([ query ] + params).inspect) 146 - 147 - self.connection.execute(query, params) 148 - end 149 - 150 - def migrate_from(version) 151 - STDERR.puts "migrating db from version #{version}" 152 - 153 - case version 154 - when 0 155 - # we created a new db from scratch, no need to migrate to anything 156 - @db.execute("INSERT INTO schema_version (version) " << 157 - "VALUES ('#{DB_VERSION}')") 158 - return 159 - 160 - when 1 161 - @db.execute(" 162 - CREATE TABLE IF NOT EXISTS 163 - folders 164 - (uuid STRING PRIMARY KEY, 165 - created_at DATETIME, 166 - updated_at DATETIME, 167 - user_uuid STRING, 168 - name BLOB) 169 - ") 170 - 171 - when 2 172 - @db.execute("ALTER TABLE ciphers ADD name BLOB") 173 - @db.execute("ALTER TABLE ciphers ADD notes BLOB") 174 - @db.execute("ALTER TABLE ciphers ADD fields BLOB") 175 - @db.execute("ALTER TABLE ciphers ADD login BLOB") 176 - @db.execute("ALTER TABLE ciphers ADD card BLOB") 177 - @db.execute("ALTER TABLE ciphers ADD identity BLOB") 178 - @db.execute("ALTER TABLE ciphers ADD securenote BLOB") 179 - 180 - # migrate each existing field in the data column to its new dedicated 181 - # field 182 - Cipher.clear_column_cache! 183 - Cipher.all.each do |c| 184 - c.migrate_data! 185 - end 186 - 187 - STDERR.puts "migrated all ciphers to new dedicated fields" 188 - end 189 - 190 - @db.execute("UPDATE schema_version SET version = #{version + 1}") 191 - end 22 + def self.connect environment: 23 + dbconfig = YAML.load(File.read('db/config.yml')) 24 + ActiveRecord::Base.establish_connection dbconfig[environment] 192 25 end 193 26 end
+6 -261
lib/dbmodel.rb
··· 14 14 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 15 # 16 16 17 - class DBModel 18 - class << self 19 - attr_reader :table_name, :columns 20 - 21 - def method_missing(method, *args, &block) 22 - if m = method.to_s.match(/^find_by_(.+)/) 23 - return find_by_column(m[1].split("_and_"), args) 24 - elsif m = method.to_s.match(/^find_all_by_(.+)/) 25 - return find_all_by_column(m[1].split("_and_"), args) 26 - else 27 - super 28 - end 29 - end 30 - 31 - def all 32 - fetch_columns 33 - 34 - Db.execute("SELECT * FROM `#{table_name}` ORDER BY `#{primary_key}`"). 35 - map do |rec| 36 - build_obj_from_rec(rec) 37 - end 38 - end 39 - 40 - # transform ruby data into sql 41 - def cast_data_for_column(data, col) 42 - if !@columns || !@columns.any? 43 - raise "need to fetch columns but in a query" 44 - end 45 - 46 - case @columns[col][:type] 47 - when /boolean/i 48 - return data == true ? 1 : 0 49 - when /datetime/i 50 - return (data == nil ? nil : data.to_i) 51 - when /integer/i 52 - return (data == nil ? nil : data.to_i) 53 - when /blob/i 54 - return (data == nil ? nil : data.to_s) 55 - else 56 - return data 57 - end 58 - end 59 - 60 - def clear_column_cache! 61 - @columns = {} 62 - end 63 - 64 - def fetch_columns 65 - return if (@columns && @columns.any?) 66 - 67 - @columns = {} 68 - 69 - Db.execute("SELECT sql FROM sqlite_master WHERE tbl_name = ?", 70 - [ self.table_name ]).first["sql"]. 71 - gsub("\n", " "). 72 - gsub(/^\s*CREATE\s+TABLE\s+#{self.table_name}\s*\(/i, ""). 73 - split(","). 74 - each do |f| 75 - if !(m = f.match(/^\s*([A-Za-z0-9_-]+)\s+([A-Za-z ]+)/)) 76 - raise "can't parse column definition #{f.inspect}" 77 - end 78 - 79 - @columns[m[1]] = { 80 - :type => m[2].strip.upcase, 81 - } 82 - end 83 - 84 - attr_accessor(*(columns.keys)) 85 - end 86 - 87 - def find_all_by_column(columns, values, limit = nil) 88 - fetch_columns 89 - 90 - if columns.count != values.count 91 - raise "arg mismatch: #{columns.inspect} vs #{values.inspect}" 92 - end 93 - 94 - where = columns.map{|c| "`#{c}` = ?" }.join(" AND ") 95 - values = values.map{|v| v.is_a?(String) ? v.encode("utf-8") : v } 96 - 97 - Db.execute("SELECT * FROM `#{table_name}` WHERE #{where} #{limit}", 98 - values).map do |rec| 99 - build_obj_from_rec(rec) 100 - end 101 - end 102 - 103 - def find_by_column(columns, values) 104 - find_all_by_column(columns, values, "LIMIT 1").first 105 - end 106 - 107 - def first 108 - fetch_columns 109 - 110 - rec = Db.execute("SELECT * FROM `#{table_name}` ORDER BY " << 111 - "`#{primary_key}` LIMIT 1").first 112 - 113 - rec ? build_obj_from_rec(rec) : nil 114 - end 115 - 116 - def last 117 - fetch_columns 118 - 119 - rec = Db.execute("SELECT * FROM `#{table_name}` ORDER BY " << 120 - "`#{primary_key}` DESC LIMIT 1").first 121 - 122 - rec ? build_obj_from_rec(rec) : nil 123 - end 124 - 125 - def primary_key 126 - @primary_key || "id" 127 - end 128 - 129 - def primary_key_uuid? 130 - !!primary_key.match(/uuid$/) 131 - end 132 - 133 - def set_primary_key(col) 134 - @primary_key = col 135 - end 136 - 137 - def set_table_attrs(attrs) 138 - @table_attrs = attrs 139 - attr_accessor(*attrs) 140 - end 141 - 142 - def set_table_name(table) 143 - @table_name = table 144 - end 145 - 146 - # transform database data into ruby 147 - def uncast_data_from_column(data, col) 148 - if !@columns || !@columns.any? 149 - raise "need to fetch columns but in a query" 150 - end 151 - 152 - case @columns[col][:type] 153 - when /boolean/i 154 - return (data >= 1) 155 - when /datetime/i 156 - return (data == nil ? nil : Time.at(data.to_i)) 157 - when /integer/i 158 - return (data == nil ? nil : data.to_i) 159 - else 160 - return data 161 - end 162 - end 163 - 164 - def writable_columns_for(what) 165 - k = columns.keys 166 - 167 - # normally we don't want to include `id` in the insert/update because 168 - # the db handles that for us, but when we have uuid primary keys, we 169 - # need to generate them ourselves, so include the column 170 - unless what == :insert && primary_key_uuid? 171 - k.reject!{|a| a == primary_key } 172 - end 173 - 174 - k 175 - end 176 - 177 - private 178 - def build_obj_from_rec(rec) 179 - obj = self.new 180 - obj.new_record = false 181 - 182 - rec.each do |k,v| 183 - next if !k.is_a?(String) 184 - obj.send("#{k}=", uncast_data_from_column(v, k)) 185 - end 186 - 187 - obj 188 - end 189 - end 190 - 191 - attr_accessor :new_record 192 - 193 - def self.transaction(&block) 194 - ret = true 195 - 196 - Db.connection.transaction do 197 - ret = yield block 198 - end 199 - 200 - ret 201 - end 202 - 203 - def initialize 204 - @new_record = true 205 - end 206 - 207 - def method_missing(method, *args, &block) 208 - self.class.fetch_columns 209 - super 210 - end 211 - 212 - def actual_before_create 213 - if self.class.primary_key_uuid? && self.send(self.class.primary_key).blank? 214 - self.send("#{self.class.primary_key}=", SecureRandom.uuid) 215 - end 216 - 217 - if self.class.columns["created_at"] 218 - self.created_at = Time.now 219 - end 220 - 221 - before_create 222 - end 223 - 224 - def actual_before_save 225 - if self.class.columns["updated_at"] 226 - self.updated_at = Time.now 227 - end 228 - 229 - before_save 230 - end 231 - 232 - def before_create 233 - true 234 - end 235 - 236 - def before_save 237 - true 238 - end 239 - 240 - def destroy 241 - if !self.new_record && self.send(self.class.primary_key) 242 - Db.execute("DELETE FROM `#{self.class.table_name}` WHERE " << 243 - "`#{self.class.primary_key}` = ?", 244 - [ self.send(self.class.primary_key) ]) 245 - end 246 - end 247 - 248 - def save 249 - self.class.fetch_columns 250 - 251 - return false if !self.actual_before_save 252 - 253 - if self.new_record 254 - return false if !self.actual_before_create 255 - 256 - Db.execute("INSERT INTO `#{self.class.table_name}` (" << 257 - self.class.writable_columns_for(:insert).map{|a| "`#{a.to_s}`" }. 258 - join(", ") << 259 - ") VALUES (" << 260 - self.class.writable_columns_for(:insert).map{|a| "?" }.join(", ") << 261 - ")", 262 - self.class.writable_columns_for(:insert).map{|a| 263 - self.class.cast_data_for_column(self.send(a), a) 264 - }) 265 - 266 - self.new_record = false 267 - else 268 - Db.execute("UPDATE `#{self.class.table_name}` SET " + 269 - self.class.writable_columns_for(:update).map{|a| "`#{a.to_s}` = ?" }. 270 - join(", ") << 271 - " WHERE `#{self.class.primary_key}` = ?", 272 - self.class.writable_columns_for(:update).map{|a| 273 - self.class.cast_data_for_column(self.send(a), a) 274 - } + [ self.send(self.class.primary_key) ]) 275 - end 276 - 277 - true 17 + class DBModel < ActiveRecord::Base 18 + self.inheritance_column = "inheritance_type" 19 + self.abstract_class = true 20 + protected 21 + def generate_uuid_primary_key 22 + self.id = SecureRandom.uuid 278 23 end 279 24 end
+5 -7
lib/device.rb
··· 15 15 # 16 16 17 17 class Device < DBModel 18 - set_table_name "devices" 19 - set_primary_key "uuid" 18 + self.table_name = "devices" 19 + #set_primary_key "uuid" 20 20 21 - attr_writer :user 21 + before_create :generate_uuid_primary_key 22 + 23 + belongs_to :user, foreign_key: :user_uuid, inverse_of: :devices 22 24 23 25 DEFAULT_TOKEN_VALIDITY = (60 * 60) 24 26 ··· 45 47 :scope => [ "api", "offline_access" ], 46 48 :amr => [ "Application" ], 47 49 }) 48 - end 49 - 50 - def user 51 - @user ||= User.find_by_uuid(self.user_uuid) 52 50 end 53 51 end
+6 -7
lib/folder.rb
··· 15 15 # 16 16 17 17 class Folder < DBModel 18 - set_table_name "folders" 19 - set_primary_key "uuid" 18 + self.table_name = "folders" 19 + #set_primary_key "uuid" 20 + 21 + before_create :generate_uuid_primary_key 20 22 21 - attr_writer :user 23 + belongs_to :user, foreign_key: :user_uuid, inverse_of: :folders 24 + has_many :ciphers, foreign_key: :folder_uuid, inverse_of: :folder 22 25 23 26 def to_hash 24 27 { ··· 31 34 32 35 def update_from_params(params) 33 36 self.name = params[:name] 34 - end 35 - 36 - def user 37 - @user ||= User.find_by_uuid(self.user_uuid) 38 37 end 39 38 end
-18
lib/helper.rb
··· 24 24 end 25 25 end 26 26 27 - class NilClass 28 - def blank? 29 - true 30 - end 31 - 32 - def present? 33 - false 34 - end 35 - end 36 - 37 27 class String 38 - def blank? 39 - self.strip == "" 40 - end 41 - 42 - def present? 43 - !blank? 44 - end 45 - 46 28 def timingsafe_equal_to(other) 47 29 if self.bytesize != other.bytesize 48 30 return false
+13 -22
lib/user.rb
··· 17 17 require "rotp" 18 18 19 19 class User < DBModel 20 - set_table_name "users" 21 - set_primary_key "uuid" 20 + self.table_name = "users" 21 + #set_primary_key "uuid" 22 22 23 - def before_save 24 - if self.security_stamp.blank? 25 - self.security_stamp = SecureRandom.uuid 26 - end 27 - true 28 - end 23 + before_create :generate_uuid_primary_key 24 + before_validation :generate_security_stamp 29 25 30 - def ciphers 31 - @ciphers ||= Cipher.find_all_by_user_uuid(self.uuid). 32 - each{|d| d.user = self } 33 - end 26 + has_many :ciphers, foreign_key: :user_uuid, inverse_of: :user 27 + has_many :folders, foreign_key: :user_uuid, inverse_of: :user 28 + has_many :devices, foreign_key: :user_uuid, inverse_of: :user 34 29 35 30 def decrypt_data_with_master_password_key(data, mk) 36 31 # self.key is random data encrypted with the key of (password,email), so ··· 40 35 Bitwarden.decrypt(data, encKey[0, 32], encKey[32, 32]) 41 36 end 42 37 43 - def devices 44 - @devices ||= Device.find_all_by_user_uuid(self.uuid). 45 - each{|d| d.user = self } 46 - end 47 - 48 38 def encrypt_data_with_master_password_key(data, mk) 49 39 # self.key is random data encrypted with the key of (password,email), so 50 40 # create that key and decrypt the random data to get the original 51 41 # encryption key, then use that key to encrypt the data 52 42 encKey = Bitwarden.decrypt(self.key, mk[0, 32], mk[32, 32]) 53 43 Bitwarden.encrypt(data, encKey[0, 32], encKey[32, 32]) 54 - end 55 - 56 - def folders 57 - @folders ||= Folder.find_all_by_user_uuid(self.uuid). 58 - each{|f| f.user = self } 59 44 end 60 45 61 46 def has_password_hash?(hash) ··· 100 85 101 86 def verifies_totp_code?(code) 102 87 ROTP::TOTP.new(self.totp_secret).now == code.to_s 88 + end 89 + protected 90 + def generate_security_stamp 91 + if self.security_stamp.blank? 92 + self.security_stamp = SecureRandom.uuid 93 + end 103 94 end 104 95 end
+3 -3
spec/db_spec.rb
··· 18 18 19 19 uuid = u.uuid 20 20 21 - User.find_all_by_email_and_culture(u.email, "en-US").first.uuid.must_equal uuid 22 - User.find_by_email_and_culture(u.email, "en-US").uuid.must_equal uuid 23 - User.find_by_email_and_culture(u.email, "en-NO").must_be_nil 21 + User.where(email: u.email, culture: "en-US").all.first.uuid.must_equal uuid 22 + User.find_by(email: u.email, culture: "en-US").uuid.must_equal uuid 23 + User.find_by(email: u.email, culture: "en-NO").must_be_nil 24 24 end 25 25 end
+5
spec/spec_helper.rb
··· 14 14 require File.realpath(File.dirname(__FILE__) + "/../lib/bitwarden_ruby.rb") 15 15 require "#{APP_ROOT}/lib/app.rb" 16 16 17 + #load 'db/schema.rb' 18 + ActiveRecord::Migrator.up "db/migrate" 19 + 17 20 include Rack::Test::Methods 21 + 22 + #ActiveRecord::Migration.maintain_test_schema! 18 23 19 24 def last_json_response 20 25 JSON.parse(last_response.body)
+69
tools/migrate_to_ar.rb
··· 1 + # Tool to migrate old. "manually managed" database to active-record + standalone_migrations 2 + ## Necessary steps 3 + # 1. Create backup of old database 4 + # 2. Initialize new database with AR 5 + # 3. Migrate data from old to ar-db 6 + # 4. Profit! 7 + 8 + require 'getoptlong' 9 + 10 + def usage 11 + puts "usage: #{$PROGRAM_NAME} -e development" 12 + exit 1 13 + end 14 + 15 + environment = nil 16 + begin 17 + GetoptLong.new( 18 + ['--environment', '-e', GetoptLong::REQUIRED_ARGUMENT] 19 + ).each do |opt, arg| 20 + case opt 21 + when '--environment' 22 + environment = arg 23 + end 24 + end 25 + rescue GetoptLong::InvalidOption 26 + usage 27 + end 28 + 29 + usage unless environment 30 + 31 + 32 + require 'yaml_db' 33 + require 'fileutils' 34 + require File.realpath(File.dirname(__FILE__) + "/../lib/bitwarden_ruby.rb") 35 + ActiveRecord::Base.remove_connection 36 + 37 + data_file = "db/dump.yml" 38 + 39 + dbconfig = YAML.load(File.read('db/config.yml')) 40 + ActiveRecord::Base.establish_connection dbconfig[environment] 41 + 42 + # select only tables for defined models 43 + class YamlDb::SerializationHelper::Dump 44 + def self.tables 45 + #ActiveRecord::Base.connection.tables.reject { |table| ['schema_info', 'schema_migrations', 'schema_version'].include?(table) }.sort 46 + ObjectSpace.each_object(Class).select {|k| k < DBModel}.map {|k| k.table_name } 47 + end 48 + end 49 + 50 + YamlDb::SerializationHelper::Base.new(YamlDb::Helper).dump(data_file) 51 + 52 + ActiveRecord::Base.remove_connection 53 + 54 + FileUtils.mv dbconfig[environment]["database"], "#{dbconfig[environment]["database"]}.#{Time.now.to_i}" 55 + 56 + system "rake db:migrate RACK_ENV=#{environment}" 57 + 58 + ActiveRecord::Base.establish_connection dbconfig[environment] 59 + YamlDb::SerializationHelper::Base.new(YamlDb::Helper).load(data_file) 60 + FileUtils.rm data_file 61 + 62 + # reset created_at / updated_at from seconds since epoch to actual datetime for ar magic 63 + DBModel.record_timestamps = false 64 + ObjectSpace.each_object(Class).select {|k| k < DBModel}.each do |k| 65 + k.all.each do |i| 66 + i.update created_at: Time.at(i.created_at), updated_at: Time.at(i.updated_at) 67 + end 68 + end 69 + DBModel.record_timestamps = true