diff --git a/API.md b/API.md index 2c2355b..0bd101d 100644 --- a/API.md +++ b/API.md @@ -309,7 +309,7 @@ different). The `deviceIdentifier` is a random UUID generated by the device and remains constant across logins. -`deviceType` is `2` +`deviceType` is `3` [for Firefox](https://github.com/bitwarden/core/blob/c9a2e67d0965fd046a0b3099e9511c26f0201acd/src/Core/Enums/DeviceType.cs). POST $identityURL/connect/token diff --git a/AR-MIGRATE.md b/AR-MIGRATE.md deleted file mode 100644 index 2ed62ad..0000000 --- a/AR-MIGRATE.md +++ /dev/null @@ -1,59 +0,0 @@ -## Rubywarden - -### Migrating From `bitwarden-ruby` to Rubywarden and ActiveRecord - -If you've used this application before it switched to using ActiveRecord -(when it was called `bitwarden-ruby`), you need to do the following steps to -migrate the data and generate the new table structures. - -Even though the migration script will import to a new database file at a -different path, 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. - -First make sure you have the latest code: - - git pull - -Then checkout to a specific revision where the migration was made: - - git checkout 40044728d - -Run `bundle` to add some required libraries for the migration: - - bundle --with migrate - -Now you are ready to do the migration: - - bundle exec ruby tools/migrate_to_ar.rb -e production - -The `-e` switch allows you to select the correct database environment from -`db/config.yml`. - -The migration script will: - - - dump the contents of the old database (most likely at - `db/production.sqlite`) to a temporary YAML file - - create the new database at `db/production/production.sqlite3` using - ActiveRecord migrations - - import the contents from the dump file - - remove the dump file - -Now your data is completely migrated into a new database at the new recommended -path, and the library will now use ActiveRecord to handle anything database -related. - -It is recommended to follow the -[initial installation instructions](https://github.com/jcs/rubywarden#manual-setup) -to create a new, unprivileged user to own the new `db/production/` database -and run the server. - -Lastly, update to the current code: - - git checkout master - -And then follow the -[update instructions](https://github.com/jcs/rubywarden#updating) -to bring your database up to date with the latest migrations. diff --git a/Gemfile b/Gemfile index 8579076..c6a07a5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,14 @@ source "https://rubygems.org" -ruby ">= 2.2.8" +ruby ">= 2.4.0" -gem "rack", ">= 2.0.6" +gem "rack", "~> 2.2" -gem "sinatra", "~> 2.0.3" -gem "sinatra-contrib", "~> 2.0.3" +gem "sinatra", "~> 2.2" +gem "sinatra-contrib", "~> 2.2" -gem "activerecord", "~> 5.1.5" -gem "sinatra-activerecord", "~> 2.0.13" +gem "activerecord", "~> 5.2" +gem "sinatra-activerecord", "~> 2.0" gem "sqlite3" gem "unicorn" @@ -30,8 +30,4 @@ group :keepass, :optional => true do gem 'rubeepass', '~> 3.3' end -group :migrate, optional: true do - gem 'yaml_db' -end - gem 'pry' diff --git a/Gemfile.lock b/Gemfile.lock index 891427b..7ab9e47 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,190 +1,112 @@ GEM remote: https://rubygems.org/ specs: - actioncable (5.1.6) - actionpack (= 5.1.6) - nio4r (~> 2.0) - websocket-driver (~> 0.6.1) - actionmailer (5.1.6) - actionpack (= 5.1.6) - actionview (= 5.1.6) - activejob (= 5.1.6) - mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 2.0) - actionpack (5.1.6) - actionview (= 5.1.6) - activesupport (= 5.1.6) - rack (~> 2.0) - rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.1.6) - activesupport (= 5.1.6) - builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.1.6) - activesupport (= 5.1.6) - globalid (>= 0.3.6) - activemodel (5.1.6) - activesupport (= 5.1.6) - activerecord (5.1.6) - activemodel (= 5.1.6) - activesupport (= 5.1.6) - arel (~> 8.0) - activesupport (5.1.6) + activemodel (5.2.8.1) + activesupport (= 5.2.8.1) + activerecord (5.2.8.1) + activemodel (= 5.2.8.1) + activesupport (= 5.2.8.1) + arel (>= 9.0) + activesupport (5.2.8.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - arel (8.0.0) - backports (3.11.3) - builder (3.2.3) - chunky_png (1.3.10) - coderay (1.1.2) - concurrent-ruby (1.0.5) - crass (1.0.4) - djinni (2.2.4) - fagin (~> 1.2, >= 1.2.1) - erubi (1.7.1) - fagin (1.2.1) - globalid (0.4.1) - activesupport (>= 4.2.0) - hilighter (1.2.3) - i18n (1.0.1) + arel (9.0.0) + chunky_png (1.4.0) + coderay (1.1.3) + concurrent-ruby (1.1.10) + djinni (2.2.5) + fagin (~> 1.2, >= 1.2.2) + fagin (1.2.2) + hilighter (1.5.9) + i18n (1.11.0) concurrent-ruby (~> 1.0) - json (2.1.0) - json_config (0.1.4) - jwt (2.1.0) - kgio (2.11.2) - loofah (2.2.3) - crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.7.0) - mini_mime (>= 0.1.1) - method_source (0.9.0) - mini_mime (1.0.0) - mini_portile2 (2.3.0) - minitest (5.11.3) - multi_json (1.13.1) - mustermann (1.0.2) - nio4r (2.3.1) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) - os (1.0.0) + json (2.6.2) + jsoncfg (1.2.11) + jwt (2.3.0) + kgio (2.11.4) + method_source (1.0.0) + minitest (5.15.0) + multi_json (1.15.0) + mustermann (1.1.1) + ruby2_keywords (~> 0.0.1) + os (1.1.4) pbkdf2-ruby (0.2.1) - pry (0.11.3) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - rack (2.0.6) - rack-protection (2.0.3) + pry (0.14.1) + coderay (~> 1.1) + method_source (~> 1.0) + rack (2.2.3.1) + rack-protection (2.2.0) rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (5.1.6) - actioncable (= 5.1.6) - actionmailer (= 5.1.6) - actionpack (= 5.1.6) - actionview (= 5.1.6) - activejob (= 5.1.6) - activemodel (= 5.1.6) - activerecord (= 5.1.6) - activesupport (= 5.1.6) - bundler (>= 1.3.0) - railties (= 5.1.6) - sprockets-rails (>= 2.0.0) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) - nokogiri (>= 1.6) - rails-html-sanitizer (1.0.4) - loofah (~> 2.2, >= 2.2.2) - railties (5.1.6) - actionpack (= 5.1.6) - activesupport (= 5.1.6) - method_source - rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) - raindrops (0.19.0) - rake (12.3.1) - rotp (3.3.1) - rqrcode (0.10.1) + raindrops (0.20.0) + rake (13.0.6) + rotp (6.2.0) + rqrcode (2.1.1) chunky_png (~> 1.0) - rubeepass (3.3.0) - djinni (~> 2.2, >= 2.2.4) - hilighter (~> 1.1, >= 1.2.3) - json_config (~> 0.1, >= 0.1.4) - os (~> 1.0, >= 1.0.0) + rqrcode_core (~> 1.0) + rqrcode_core (1.2.0) + rubeepass (3.5.0) + djinni (~> 2.2, >= 2.2.5) + hilighter (~> 1.5, >= 1.5.1) + jsoncfg (~> 1.2, >= 1.2.11) + os (~> 1.0, >= 1.0.1) salsa20 (~> 0.1, >= 0.1.3) - scoobydoo (~> 0.1, >= 0.1.6) + scoobydoo (~> 1.0, >= 1.0.1) twofish (~> 1.0, >= 1.0.8) + ruby2_keywords (0.0.5) salsa20 (0.1.3) - scoobydoo (0.1.6) - sinatra (2.0.3) + scoobydoo (1.0.1) + sinatra (2.2.0) mustermann (~> 1.0) - rack (~> 2.0) - rack-protection (= 2.0.3) + rack (~> 2.2) + rack-protection (= 2.2.0) tilt (~> 2.0) - sinatra-activerecord (2.0.13) - activerecord (>= 3.2) + sinatra-activerecord (2.0.25) + activerecord (>= 4.1) sinatra (>= 1.0) - sinatra-contrib (2.0.3) - activesupport (>= 4.0.0) - backports (>= 2.8.2) + sinatra-contrib (2.2.0) multi_json mustermann (~> 1.0) - rack-protection (= 2.0.3) - sinatra (= 2.0.3) - tilt (>= 1.3, < 3) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) - sprockets (>= 3.0.0) - sqlite3 (1.3.13) - thor (0.20.0) + rack-protection (= 2.2.0) + sinatra (= 2.2.0) + tilt (~> 2.0) + sqlite3 (1.4.2) thread_safe (0.3.6) - tilt (2.0.8) + tilt (2.0.10) twofish (1.0.8) - tzinfo (1.2.5) + tzinfo (1.2.10) thread_safe (~> 0.1) - unicorn (5.4.1) + unicorn (6.1.0) kgio (~> 2.6) raindrops (~> 0.7) - websocket-driver (0.6.5) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) - yaml_db (0.7.0) - rails (>= 3.0) - rake (>= 0.8.7) PLATFORMS ruby DEPENDENCIES - activerecord (~> 5.1.5) + activerecord (~> 5.2) json jwt minitest pbkdf2-ruby pry - rack (>= 2.0.6) + rack (~> 2.2) rack-test rake rotp rqrcode rubeepass (~> 3.3) - sinatra (~> 2.0.3) - sinatra-activerecord (~> 2.0.13) - sinatra-contrib (~> 2.0.3) + sinatra (~> 2.2) + sinatra-activerecord (~> 2.0) + sinatra-contrib (~> 2.2) sqlite3 unicorn - yaml_db RUBY VERSION ruby 2.4.2p198 BUNDLED WITH - 1.17.1 + 1.17.2 diff --git a/README.md b/README.md index 7aa6939..9cdefd2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -**This project is not associated with the +**This project is no longer being maintained. Please see +[this issue](https://github.com/jcs/rubywarden/issues/122) +for further information.** + +This project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC. Do not contact Bitwarden for support with using this backend server -(or at the very least, make it abundantly clear that you are using a 3rd party backend server).** +(or at the very least, make it abundantly clear that you are using a 3rd party backend server). ## Rubywarden @@ -99,11 +103,6 @@ Then you can configure the Bitwarden clients with a single server URL of ### Updating -If you've previously used Rubywarden before July 30, 2018 when it was called -`bitwarden-ruby`, when it did not use ActiveRecord, you should instead -[migrate](AR-MIGRATE.md) -your existing database. - To update your instance of Rubywarden, fetch the latest code: cd /path/to/your/rubywarden diff --git a/Rakefile b/Rakefile index 6866299..e6c9f3e 100644 --- a/Rakefile +++ b/Rakefile @@ -12,4 +12,6 @@ require 'rake/testtask' Rake::TestTask.new do |t| t.libs << "spec" t.pattern = "spec/*_spec.rb" -end \ No newline at end of file +end + +task :default => [ :test ] diff --git a/db/migrate/20190526014920_password_history.rb b/db/migrate/20190526014920_password_history.rb new file mode 100644 index 0000000..c7374ee --- /dev/null +++ b/db/migrate/20190526014920_password_history.rb @@ -0,0 +1,5 @@ +class PasswordHistory < ActiveRecord::Migration[5.1] + def change + add_column :ciphers, :passwordhistory, :binary + end +end diff --git a/db/migrate/20190724163354_no_null_kdf_iterations.rb b/db/migrate/20190724163354_no_null_kdf_iterations.rb new file mode 100644 index 0000000..8d12476 --- /dev/null +++ b/db/migrate/20190724163354_no_null_kdf_iterations.rb @@ -0,0 +1,18 @@ +class NoNullKdfIterations < ActiveRecord::Migration[5.1] + def change + User.all.each do |u| + # any old users without a kdf_iterations value probably have the old + # value of 5000 + if !u.kdf_iterations + u.kdf_iterations = 5000 + u.kdf_type = Bitwarden::KDF::PBKDF2 + end + u.save! + end + + # but going forward, any new users should get whatever defaults are set in + # the future + change_column :users, :kdf_iterations, :integer, :null => false, + :default => Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE] + end +end diff --git a/lib/app.rb b/lib/app.rb index 4785f04..f7db2d2 100644 --- a/lib/app.rb +++ b/lib/app.rb @@ -30,7 +30,7 @@ class App < Sinatra::Base register Sinatra::Namespace register Sinatra::ActiveRecordExtension - set :root, File.dirname(__FILE__) + set :root, File.expand_path("..", File.dirname(__FILE__)) configure do enable :logging @@ -55,11 +55,27 @@ class App < Sinatra::Base # we're always going to reply with json content_type :json + + # set CORS headers for safari extension + response.headers["Access-Control-Allow-Origin"] = "file://" + # just parrot back whatever safari asked for + if request.env["HTTP_ACCESS_CONTROL_REQUEST_METHOD"] + response.headers["Access-Control-Allow-Methods"] = + request.env["HTTP_ACCESS_CONTROL_REQUEST_METHOD"] + end + if request.env["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"] + response.headers["Access-Control-Allow-Headers"] = + request.env["HTTP_ACCESS_CONTROL_REQUEST_HEADERS"] + end end register Rubywarden::Routing::Api register Rubywarden::Routing::Icons register Rubywarden::Routing::Identity register Rubywarden::Routing::Attachments + + options /.*/ do + # empty response just to respond with CORS headers + end end end diff --git a/lib/bitwarden.rb b/lib/bitwarden.rb index 8f6a90d..7c2929d 100644 --- a/lib/bitwarden.rb +++ b/lib/bitwarden.rb @@ -29,7 +29,7 @@ class Bitwarden::KDF TYPE_IDS = TYPES.invert DEFAULT_ITERATIONS = { - PBKDF2 => 5000, + PBKDF2 => 100_000, } ITERATION_RANGES = { diff --git a/lib/cipher.rb b/lib/cipher.rb index a9dd0f7..b1b5950 100644 --- a/lib/cipher.rb +++ b/lib/cipher.rb @@ -29,6 +29,7 @@ class Cipher < DBModel serialize :securenote, JSON serialize :card, JSON serialize :identity, JSON + serialize :passwordhistory, JSON TYPE_LOGIN = 1 TYPE_NOTE = 2 @@ -99,6 +100,7 @@ def to_hash "Card" => self.card, "Identity" => self.identity, "SecureNote" => self.securenote, + "PasswordHistory" => self.passwordhistory, } end @@ -126,6 +128,13 @@ def update_from_params(params) self.login = tlogin + if params[:passwordhistory].present? + self.passwordhistory = params[:passwordhistory]. + map{|ph| ph.ucfirst_hash } + else + self.passwordhistory = nil + end + when TYPE_NOTE self.securenote = params[:securenote].ucfirst_hash diff --git a/lib/folder.rb b/lib/folder.rb index 09a83fc..f0079bc 100644 --- a/lib/folder.rb +++ b/lib/folder.rb @@ -21,7 +21,10 @@ class Folder < DBModel before_create :generate_uuid_primary_key belongs_to :user, foreign_key: :user_uuid, inverse_of: :folders - has_many :ciphers, foreign_key: :folder_uuid, inverse_of: :folder + has_many :ciphers, + foreign_key: :folder_uuid, + inverse_of: :folder, + dependent: :nullify def to_hash { diff --git a/lib/routes/api.rb b/lib/routes/api.rb index b02f89e..c54e3d1 100644 --- a/lib/routes/api.rb +++ b/lib/routes/api.rb @@ -208,6 +208,11 @@ def self.registered(app) delete_cipher app: app, uuid: params[:uuid] end + # delete a cipher (new client) + put "/ciphers/:uuid/delete" do + delete_cipher app: app, uuid: params[:uuid] + end + # # folders # diff --git a/lib/routes/attachments.rb b/lib/routes/attachments.rb index 748256f..4b3273c 100644 --- a/lib/routes/attachments.rb +++ b/lib/routes/attachments.rb @@ -20,6 +20,10 @@ module Attachments def self.registered(app) app.namespace BASE_URL do post "/ciphers/:uuid/attachment" do + if !device_from_bearer + return validation_error("invalid bearer") + end + cipher = retrieve_cipher uuid: params[:uuid] need_params(:data) do |p| @@ -62,6 +66,8 @@ def self.registered(app) app.namespace ATTACHMENTS_URL do get "/:uuid/:attachment_id" do + # no device authentication + a = Attachment.find_by_uuid_and_cipher_uuid(params[:attachment_id], params[:uuid]) attachment(a.filename) diff --git a/lib/routes/identity.rb b/lib/routes/identity.rb index 9b8bac5..0fd4ac7 100644 --- a/lib/routes/identity.rb +++ b/lib/routes/identity.rb @@ -110,6 +110,8 @@ def self.registered(app) :token_type => "Bearer", :refresh_token => d.refresh_token, :Key => d.user.key, + :Kdf => d.user.kdf_type, + :KdfIterations => d.user.kdf_iterations, # TODO: when to include :privateKey and :TwoFactorToken? }.to_json end diff --git a/lib/user.rb b/lib/user.rb index 0fb480e..f469390 100644 --- a/lib/user.rb +++ b/lib/user.rb @@ -25,9 +25,18 @@ class User < DBModel before_create :generate_uuid_primary_key before_validation :generate_security_stamp - has_many :ciphers, foreign_key: :user_uuid, inverse_of: :user - has_many :folders, foreign_key: :user_uuid, inverse_of: :user - has_many :devices, foreign_key: :user_uuid, inverse_of: :user + has_many :ciphers, + foreign_key: :user_uuid, + inverse_of: :user, + dependent: :destroy + has_many :folders, + foreign_key: :user_uuid, + inverse_of: :user, + dependent: :destroy + has_many :devices, + foreign_key: :user_uuid, + inverse_of: :user, + dependent: :destroy def decrypt_data_with_master_password_key(data, mk) # self.key is random data encrypted with the key of (password,email), so @@ -71,7 +80,8 @@ def two_factor_enabled? self.totp_secret.present? end - def update_master_password(old_pwd, new_pwd) + def update_master_password(old_pwd, new_pwd, + new_kdf_iterations = self.kdf_iterations) # original random encryption key must be preserved, just re-encrypted with # a new key derived from the new password @@ -81,10 +91,11 @@ def update_master_password(old_pwd, new_pwd) self.key = Bitwarden.encrypt(orig_key, Bitwarden.makeKey(new_pwd, self.email, - Bitwarden::KDF::TYPES[self.kdf_type], self.kdf_iterations)).to_s + Bitwarden::KDF::TYPES[self.kdf_type], new_kdf_iterations)).to_s self.password_hash = Bitwarden.hashPassword(new_pwd, self.email, - self.kdf_type, self.kdf_iterations) + self.kdf_type, new_kdf_iterations) + self.kdf_iterations = new_kdf_iterations self.security_stamp = SecureRandom.uuid end diff --git a/spec/attachment_spec.rb b/spec/attachment_spec.rb index ad843f1..a8f623a 100644 --- a/spec/attachment_spec.rb +++ b/spec/attachment_spec.rb @@ -8,39 +8,8 @@ before do User.all.delete_all - post "/api/accounts/register", { - :name => nil, - :email => "api@example.com", - :masterPasswordHash => Bitwarden.hashPassword("asdf", "api@example.com", - User::DEFAULT_KDF_TYPE, - Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), - :masterPasswordHint => nil, - :key => Bitwarden.makeEncKey( - Bitwarden.makeKey("adsf", "api@example.com", - User::DEFAULT_KDF_TYPE, - Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), - ), - :kdf => Bitwarden::KDF::TYPE_IDS[User::DEFAULT_KDF_TYPE], - :kdfIterations => Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE], - } - last_response.status.must_equal 200 - - post "/identity/connect/token", { - :grant_type => "password", - :username => "api@example.com", - :password => Bitwarden.hashPassword("asdf", "api@example.com", - User::DEFAULT_KDF_TYPE, - Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), - :scope => "api offline_access", - :client_id => "browser", - :deviceType => 3, - :deviceIdentifier => SecureRandom.uuid, - :deviceName => "firefox", - :devicePushToken => "" - } - last_response.status.must_equal 200 - - @access_token = last_json_response["access_token"] + Rubywarden::Test::Factory.create_user + @access_token = Rubywarden::Test::Factory.login_user post_json "/api/ciphers", { :type => 1, diff --git a/spec/bitwarden_importer_spec.rb b/spec/bitwarden_importer_spec.rb index 4f9ce5a..0f96014 100644 --- a/spec/bitwarden_importer_spec.rb +++ b/spec/bitwarden_importer_spec.rb @@ -10,21 +10,7 @@ @master_key = Bitwarden.makeKey(@password, @email, User::DEFAULT_KDF_TYPE, Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]) - - post "/api/accounts/register", { - :name => nil, - :email => @email, - :masterPasswordHash => Bitwarden.hashPassword(@password, @email, - User::DEFAULT_KDF_TYPE, - Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), - :masterPasswordHint => nil, - :key => Bitwarden.makeEncKey(@master_key), - :kdf => Bitwarden::KDF::TYPE_IDS[User::DEFAULT_KDF_TYPE], - :kdfIterations => Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE], - } - last_response.status.must_equal 200 - - @user = User.where(:email => @email).first! + @user = Rubywarden::Test::Factory.create_user email: @email, password: @password end it "imports all expected data" do diff --git a/spec/cipher_spec.rb b/spec/cipher_spec.rb index fc984f3..a39f88c 100644 --- a/spec/cipher_spec.rb +++ b/spec/cipher_spec.rb @@ -6,39 +6,8 @@ before do User.all.delete_all - post "/api/accounts/register", { - :name => nil, - :email => "api@example.com", - :masterPasswordHash => Bitwarden.hashPassword("asdf", "api@example.com", - User::DEFAULT_KDF_TYPE, - Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), - :masterPasswordHint => nil, - :key => Bitwarden.makeEncKey( - Bitwarden.makeKey("adsf", "api@example.com", - User::DEFAULT_KDF_TYPE, - Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), - ), - :kdf => Bitwarden::KDF::TYPE_IDS[User::DEFAULT_KDF_TYPE], - :kdfIterations => Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE], - } - last_response.status.must_equal 200 - - post "/identity/connect/token", { - :grant_type => "password", - :username => "api@example.com", - :password => Bitwarden.hashPassword("asdf", "api@example.com", - User::DEFAULT_KDF_TYPE, - Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), - :scope => "api offline_access", - :client_id => "browser", - :deviceType => 3, - :deviceIdentifier => SecureRandom.uuid, - :deviceName => "firefox", - :devicePushToken => "" - } - last_response.status.must_equal 200 - - @access_token = last_json_response["access_token"] + Rubywarden::Test::Factory.create_user + @access_token = Rubywarden::Test::Factory.login_user end it "should not allow access with bogus bearer token" do diff --git a/spec/fixtures/chrome_export.csv b/spec/fixtures/chrome_export.csv new file mode 100644 index 0000000..1e54565 --- /dev/null +++ b/spec/fixtures/chrome_export.csv @@ -0,0 +1,5 @@ +name,url,username,password +cloud.digitalocean.com,https://cloud.digitalocean.com/login,root,mySuper.Password1 +m.facebook.com,https://m.facebook.com/,admin,mySuper?Password2 +github.com,https://github.com/login,supervisor,mySuper%Password3 +gitlab.com,https://gitlab.com/users/sign_in,moderator,mySuper-Password4 diff --git a/spec/folder_spec.rb b/spec/folder_spec.rb index 4b9393d..b5bb902 100644 --- a/spec/folder_spec.rb +++ b/spec/folder_spec.rb @@ -6,42 +6,8 @@ before do User.all.delete_all - post "/api/accounts/register", { - :name => nil, - :email => "api@example.com", - :masterPasswordHash => Bitwarden.hashPassword("asdf", "api@example.com", - User::DEFAULT_KDF_TYPE, - Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), - :masterPasswordHint => nil, - :key => Bitwarden.makeEncKey( - Bitwarden.makeKey("adsf", "api@example.com", User::DEFAULT_KDF_TYPE, - Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), - ), - :kdf => Bitwarden::KDF::TYPE_IDS[User::DEFAULT_KDF_TYPE], - :kdfIterations => Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE], - } - if last_response.status != 200 - raise last_response.inspect - end - - post "/identity/connect/token", { - :grant_type => "password", - :username => "api@example.com", - :password => Bitwarden.hashPassword("asdf", "api@example.com", - User::DEFAULT_KDF_TYPE, - Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), - :scope => "api offline_access", - :client_id => "browser", - :deviceType => 3, - :deviceIdentifier => SecureRandom.uuid, - :deviceName => "firefox", - :devicePushToken => "" - } - if last_response.status != 200 - raise last_response.inspect - end - - @access_token = last_json_response["access_token"] + Rubywarden::Test::Factory.create_user + @access_token = Rubywarden::Test::Factory.login_user end it "should not allow access with bogus bearer token" do diff --git a/spec/identity_spec.rb b/spec/identity_spec.rb index b1b3011..452f2fa 100644 --- a/spec/identity_spec.rb +++ b/spec/identity_spec.rb @@ -109,7 +109,7 @@ (u = User.find_by_email("nobody4@example.com")).wont_be_nil u.uuid.wont_be_nil - u.password_hash.must_equal "PGC1vNJZUL3z5wTKAgpXsODf6KzIPcr0XCzTplceXQU=" + u.password_hash.must_equal "uQOY5dffPoKCueMu3cMXl2KOL52NerIQlwCEpQ6mW6s=" post "/api/accounts/prelogin", { :email => "nobody4@example.com", diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0f10da3..5c87815 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,36 +18,15 @@ ActiveRecord::Migrator.up "db/migrate" # in case migrations changed what we're testing -[ User, Cipher, Device, Folder ].each do |c| +[ Attachment, User, Cipher, Device, Folder ].each do |c| c.send(:reset_column_information) end include Rack::Test::Methods -def last_json_response - JSON.parse(last_response.body) -end - -def get_json(path, params = {}, headers = {}) - json_request :get, path, params, headers -end - -def post_json(path, params = {}, headers = {}) - json_request :post, path, params, headers -end +Dir[Rubywarden::App.settings.root + '/spec/support/**/*.rb'].sort.each { |f| require f } +include Rubywarden::Test::RequestHelpers -def put_json(path, params = {}, headers = {}) - json_request :put, path, params, headers -end - -def delete_json(path, params = {}, headers = {}) - json_request :delete, path, params, headers -end - -def json_request(verb, path, params = {}, headers = {}) - send verb, path, params.to_json, - headers.merge({ "CONTENT_TYPE" => "application/json" }) -end def app Rubywarden::App diff --git a/spec/support/factories.rb b/spec/support/factories.rb new file mode 100644 index 0000000..b196720 --- /dev/null +++ b/spec/support/factories.rb @@ -0,0 +1,41 @@ +module Rubywarden + module Test + class Factory + USER_EMAIL = "user@example.com" + USER_PASSWORD = "p4ssw0rd" + + def self.create_user email: USER_EMAIL, password: USER_PASSWORD + u = User.new + u.email = email + u.kdf_type = Bitwarden::KDF::TYPE_IDS[User::DEFAULT_KDF_TYPE] + u.kdf_iterations = Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE] + u.password_hash = Bitwarden.hashPassword(password, email, + Bitwarden::KDF::TYPES[u.kdf_type], u.kdf_iterations) + u.password_hint = "it's like password but not" + u.key = Bitwarden.makeEncKey(Bitwarden.makeKey(password, email, + Bitwarden::KDF::TYPES[u.kdf_type], u.kdf_iterations)) + u.save + u + end + + def self.login_user email: USER_EMAIL, password: USER_PASSWORD + post "/identity/connect/token", { + :grant_type => "password", + :username => email, + :password => Bitwarden.hashPassword(password, email, + User::DEFAULT_KDF_TYPE, + Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE]), + :scope => "api offline_access", + :client_id => "browser", + :deviceType => 3, + :deviceIdentifier => SecureRandom.uuid, + :deviceName => "firefox", + :devicePushToken => "" + } + last_response.status.must_equal 200 + + last_json_response["access_token"] + end + end + end +end diff --git a/spec/support/request_helpers.rb b/spec/support/request_helpers.rb new file mode 100644 index 0000000..ee0e58b --- /dev/null +++ b/spec/support/request_helpers.rb @@ -0,0 +1,30 @@ +module Rubywarden + module Test + module RequestHelpers + def last_json_response + JSON.parse(last_response.body) + end + + def get_json(path, params = {}, headers = {}) + json_request :get, path, params, headers + end + + def post_json(path, params = {}, headers = {}) + json_request :post, path, params, headers + end + + def put_json(path, params = {}, headers = {}) + json_request :put, path, params, headers + end + + def delete_json(path, params = {}, headers = {}) + json_request :delete, path, params, headers + end + + def json_request(verb, path, params = {}, headers = {}) + send verb, path, params.to_json, + headers.merge({ "CONTENT_TYPE" => "application/json" }) + end + end + end +end \ No newline at end of file diff --git a/spec/user_spec.rb b/spec/user_spec.rb index 592a28b..3b79ba6 100644 --- a/spec/user_spec.rb +++ b/spec/user_spec.rb @@ -6,17 +6,7 @@ before do User.all.delete_all - - u = User.new - u.email = USER_EMAIL - u.kdf_type = Bitwarden::KDF::TYPE_IDS[User::DEFAULT_KDF_TYPE] - u.kdf_iterations = Bitwarden::KDF::DEFAULT_ITERATIONS[User::DEFAULT_KDF_TYPE] - u.password_hash = Bitwarden.hashPassword(USER_PASSWORD, USER_EMAIL, - Bitwarden::KDF::TYPES[u.kdf_type], u.kdf_iterations) - u.password_hint = "it's like password but not" - u.key = Bitwarden.makeEncKey(Bitwarden.makeKey(USER_PASSWORD, USER_EMAIL, - Bitwarden::KDF::TYPES[u.kdf_type], u.kdf_iterations)) - u.save + Rubywarden::Test::Factory.create_user email: USER_EMAIL, password: USER_PASSWORD end it "should compare a user's hash" do diff --git a/tools/change_master_password.rb b/tools/change_master_password.rb index 0eb5921..8e9637d 100644 --- a/tools/change_master_password.rb +++ b/tools/change_master_password.rb @@ -87,7 +87,26 @@ def usage end end -@u.update_master_password(password, new_master) +new_kdf_iterations = nil +while new_kdf_iterations.to_s == "" + print "kdf iterations (currently #{@u.kdf_iterations}, default " << + "#{Bitwarden::KDF::DEFAULT_ITERATIONS[@u.kdf_type]}): " + new_kdf_iterations = STDIN.gets.chomp + + if new_kdf_iterations.to_s == "" + new_kdf_iterations = Bitwarden::KDF::DEFAULT_ITERATIONS[@u.kdf_type] + break + elsif !(r = Bitwarden::KDF::ITERATION_RANGES[@u.kdf_type]). + include?(new_kdf_iterations.to_i) + puts "iterations must be between #{r}" + new_kdf_iterations = nil + else + new_kdf_iterations = new_kdf_iterations.to_i + break + end +end + +@u.update_master_password(password, new_master, new_kdf_iterations) @u.password_hint = new_master_hint if !@u.save puts "error saving new password" diff --git a/tools/chrome_import.rb b/tools/chrome_import.rb new file mode 100644 index 0000000..61b1d31 --- /dev/null +++ b/tools/chrome_import.rb @@ -0,0 +1,135 @@ +#!/usr/bin/env ruby +# +# Copyright (c) 2017 joshua stein +# Chrome importer by Haluk Unal +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +# +# Read a given Chrome CSV file, ask for the given user's master password, +# then lookup the given user in the bitwarden-ruby SQLite database and +# fetch its key. Import each entry into the bitwarden-ruby database. +# +# No check is done to eliminate duplicates, so this is best used on a fresh +# bitwarden-ruby installation after creating a new account. +# + +require File.realpath(File.dirname(__FILE__) + '/../lib/rubywarden.rb') + +require 'csv' +require 'getoptlong' + +def usage + puts "usage: #{$PROGRAM_NAME} -f data.csv -u user@example.com" + exit 1 +end + +def encrypt(str) + @u.encrypt_data_with_master_password_key(str, @master_key).to_s +end + +username = nil +file = nil +@folders = {} + +begin + GetoptLong.new( + ['--file', '-f', GetoptLong::REQUIRED_ARGUMENT], + ['--user', '-u', GetoptLong::REQUIRED_ARGUMENT] + ).each do |opt, arg| + case opt + when '--file' + file = arg + when '--user' + username = arg + end + end +rescue GetoptLong::InvalidOption + usage +end + +usage unless file && username + +@u = User.find_by_email(username) +raise "can't find existing User record for #{username.inspect}" unless @u + +print "master password for #{@u.email}: " +system('stty -echo') if STDIN.tty? +password = STDIN.gets.chomp +system('stty echo') if STDIN.tty? +puts + +unless @u.has_password_hash?(Bitwarden.hashPassword(password, @u.email, +Bitwarden::KDF::TYPES[@u.kdf_type], @u.kdf_iterations)) + raise "master password does not match stored hash" +end + +@master_key = Bitwarden.makeKey(password, @u.email, + Bitwarden::KDF::TYPES[@u.kdf_type], @u.kdf_iterations) + +@u.folders.each do |folder| + folder_name = @u.decrypt_data_with_master_password_key(folder.name, @master_key) + @folders[folder_name] = folder.uuid +end + +to_save = {} +skipped = 0 + +CSV.foreach(file, headers: true) do |row| + next if row['name'].blank? + + puts "converting #{row['name']}..." + + c = Cipher.new + c.user_uuid = @u.uuid + c.type = Cipher::TYPE_LOGIN + + cdata = { 'Name' => encrypt(row['name']) } + cdata['Uri'] = encrypt(row['url']) if row['url'].present? + cdata['Username'] = encrypt(row['username']) if row['username'].present? + cdata['Password'] = encrypt(row['password']) if row['password'].present? + + c.data = cdata.to_json + + to_save[c.type] ||= [] + to_save[c.type].push c +end + +puts + +to_save.each do |k, v| + puts "#{format('% 4d', v.count)} #{Cipher.type_s(k)}" << + (v.count == 1 ? '' : 's') +end + +puts "#{format('% 4d', skipped)} skipped" if skipped > 0 + +print 'ready to import? [Y/n] ' +exit 1 if STDIN.gets =~ /n/i + +imp = 0 +Cipher.transaction do + to_save.each_value do |v| + v.each do |c| + # TODO: convert data to each field natively and call save! on our own + c.migrate_data! + + imp += 1 + end + end +end + +puts "successfully imported #{imp} item#{imp == 1 ? '' : 's'}" + +# EOF diff --git a/tools/migrate_to_ar.rb b/tools/migrate_to_ar.rb deleted file mode 100644 index a1d1746..0000000 --- a/tools/migrate_to_ar.rb +++ /dev/null @@ -1,88 +0,0 @@ -# see https://github.com/jcs/rubywarden/blob/master/AR-MIGRATE.md - -require "fileutils" -require "getoptlong" -require "tempfile" -require "yaml_db" - -def usage - puts "usage: #{$PROGRAM_NAME} -e development" - exit 1 -end - -environment = nil -begin - GetoptLong.new( - ['--environment', '-e', GetoptLong::REQUIRED_ARGUMENT] - ).each do |opt, arg| - case opt - when '--environment' - environment = arg - end - end -rescue GetoptLong::InvalidOption - usage -end - -usage unless environment - -require File.realpath(File.dirname(__FILE__) + "/../lib/rubywarden.rb") - -ActiveRecord::Base.remove_connection - -dbconfig = YAML.load(File.read(File.realpath(__dir__ + "/../db/config.yml"))) - -# if a file exists at the new path, some kind of migration has already been -# done so bail out -newdb = dbconfig[environment]["database"] -if File.exists?(newdb) - raise "a file already exists at #{newdb}, has a migration already taken place?" -end - -olddb = File.realpath(__dir__ + "/../db/production.sqlite3") -if !olddb || !File.exists?(olddb) - raise "no file at #{olddb} to migrate" -end - -# point a temporary config at the old db path so we can dump it -tmpconfig = dbconfig[environment].dup -tmpconfig["database"] = olddb -ActiveRecord::Base.establish_connection tmpconfig - -# select only tables for defined models -class YamlDb::SerializationHelper::Dump - def self.tables - ObjectSpace.each_object(Class).select{|k| k < DBModel}.map{|k| k.table_name } - end -end - -dump_file = Tempfile.new("rubywarden-migrate") - -puts "dumping old database to #{dump_file.path}" -YamlDb::SerializationHelper::Base.new(YamlDb::Helper).dump(dump_file.path) -ActiveRecord::Base.remove_connection - -puts "creating new database at #{dbconfig[environment]["database"]}" -system("rake", "db:migrate", "RUBYWARDEN_ENV=#{environment}") - -puts "importing old database dump" -ActiveRecord::Base.establish_connection dbconfig[environment] -YamlDb::SerializationHelper::Base.new(YamlDb::Helper).load(dump_file.path) - -puts "deleting dump file" -dump_file.unlink - -# reset created_at / updated_at from seconds since epoch to actual datetime for ar magic -DBModel.record_timestamps = false -ObjectSpace.each_object(Class).select {|k| k < DBModel}.each do |k| - k.all.each do |i| - i.update created_at: Time.at(i.created_at), updated_at: Time.at(i.updated_at) - end -end -DBModel.record_timestamps = true - -newdb = File.realpath(__dir__ + "/../" + dbconfig[environment]["database"]) -puts "you may wish to delete the old database at #{newdb}" - -puts "you may also wish to create a new, unprivileged user to run the" -puts "rubywarden server and own the db/production/ directory" diff --git a/tools/mitm.rb b/tools/mitm.rb index c6aaae0..7ca2eba 100644 --- a/tools/mitm.rb +++ b/tools/mitm.rb @@ -76,7 +76,20 @@ def add_field(key, val) proxy_to upstream_url_for(request.path_info), :put end +options /(.*)/ do + proxy_to upstream_url_for(request.path_info), :options +end + def proxy_to(url, method) + if RAW_QUERIES + puts "#{request.env["REQUEST_METHOD"]} request to #{request.path_info}:" + request.env.each do |k,v| + if k.match(/^[A-Z_]+$/) + puts " #{k}: #{v}" + end + end + end + puts "proxying #{method.to_s.upcase} to #{url}" uri = URI.parse(url) @@ -121,6 +134,8 @@ def proxy_to(url, method) res = h.put(uri.path, post_data, send_headers) when :delete res = h.delete(uri.path, send_headers) + when :options + res = h.options(uri.path, send_headers) else raise "unknown method type #{method.inspect}" end