From d5d5a30702074f1c0e861e64a516fb285e3ba6aa Mon Sep 17 00:00:00 2001 From: Denis Date: Wed, 7 Aug 2019 15:24:27 +0300 Subject: [PATCH] Integrate vault-rails * Update migration --- Gemfile | 1 + Gemfile.lock | 9 +++ app/models/payment_address.rb | 24 ++++--- app/models/wallet.rb | 47 ++++++++----- bin/init_vault | 1 + config/initializers/vault.rb | 2 + ...add_encrypted_secret_to_payment_address.rb | 67 +++++++++++++++++++ db/schema.rb | 8 +-- lib/peatio/vault/totp.rb | 2 +- spec/models/payment_address_spec.rb | 33 +++++++-- spec/models/wallet_spec.rb | 28 ++++++++ 11 files changed, 186 insertions(+), 36 deletions(-) create mode 100644 db/migrate/20190807092706_add_encrypted_secret_to_payment_address.rb diff --git a/Gemfile b/Gemfile index 4711cf286a..dc6c3bc430 100644 --- a/Gemfile +++ b/Gemfile @@ -55,6 +55,7 @@ gem 'peatio', '~> 0.6.1' gem 'rack-cors', '~> 1.0.2', require: false gem 'env-tweaks', '~> 1.0.0' gem 'vault', '~> 0.12', require: false +gem 'vault-rails', '~> 0.5.0', git: 'http://github.com/rubykube/vault-rails' gem 'bootsnap', '>= 1.1.0', require: false gem 'net-http-persistent', '~> 3.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index a101558744..0da5574858 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,11 @@ +GIT + remote: http://github.com/rubykube/vault-rails + revision: ef9b8626e4bf41dcea8696c4aec91e543ddd80a5 + specs: + vault-rails (0.5.0) + rails (>= 4.1) + vault (~> 0.5) + GEM remote: https://rubygems.org/ specs: @@ -489,6 +497,7 @@ DEPENDENCIES validate_url (~> 1.0.4) validates_lengths_from_database (~> 0.7.0) vault (~> 0.12) + vault-rails (~> 0.5.0)! webmock (~> 3.5) RUBY VERSION diff --git a/app/models/payment_address.rb b/app/models/payment_address.rb index 74ba00a9d8..2db08576a1 100644 --- a/app/models/payment_address.rb +++ b/app/models/payment_address.rb @@ -2,14 +2,18 @@ # frozen_string_literal: true class PaymentAddress < ApplicationRecord + include Vault::EncryptedModel include BelongsToCurrency include BelongsToAccount + vault_lazy_decrypt! + after_commit :enqueue_address_generation validates :address, uniqueness: { scope: :currency_id }, if: :address? - serialize :details, JSON + vault_attribute :details, serialize: :json, default: {} + vault_attribute :secret before_validation do next if blockchain_api&.case_sensitive? @@ -42,18 +46,18 @@ def to_cash_address end # == Schema Information -# Schema version: 20180925123806 +# Schema version: 20190807092706 # # Table name: payment_addresses # -# id :integer not null, primary key -# currency_id :string(10) not null -# account_id :integer not null -# address :string(95) -# secret :string(128) -# details :string(1024) default({}), not null -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# currency_id :string(10) not null +# account_id :integer not null +# address :string(95) +# secret_encrypted :string(255) +# details_encrypted :string(1024) +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/app/models/wallet.rb b/app/models/wallet.rb index 450614a884..d3da401a7d 100644 --- a/app/models/wallet.rb +++ b/app/models/wallet.rb @@ -4,6 +4,10 @@ class Wallet < ApplicationRecord extend Enumerize + include Vault::EncryptedModel + + vault_lazy_decrypt! + # We use this attribute values rules for wallet kinds: # 1** - for deposit wallets. # 2** - for fee wallets. @@ -11,6 +15,7 @@ class Wallet < ApplicationRecord ENUMERIZED_KINDS = { deposit: 100, fee: 200, hot: 310, warm: 320, cold: 330 }.freeze enumerize :kind, in: ENUMERIZED_KINDS, scope: true + # Remove after admin panel deletion. SETTING_ATTRIBUTES = %i[ uri secret bitgo_test_net @@ -19,11 +24,21 @@ class Wallet < ApplicationRecord bitgo_rest_api_root bitgo_rest_api_access_token ].freeze + SETTING_ATTRIBUTES.each do |attribute| + define_method attribute do + self.settings[attribute.to_s] + end + + define_method "#{attribute}=".to_sym do |value| + self.settings = self.settings.merge(attribute.to_s => value) + end + end + NOT_AVAILABLE = 'N/A'.freeze include BelongsToCurrency - store :settings, accessors: SETTING_ATTRIBUTES, coder: JSON + vault_attribute :settings, serialize: :json, default: {} belongs_to :blockchain, foreign_key: :blockchain_key, primary_key: :key @@ -97,24 +112,24 @@ def wallet_url end # == Schema Information -# Schema version: 20181126101312 +# Schema version: 20190807092706 # # Table name: wallets # -# id :integer not null, primary key -# blockchain_key :string(32) -# currency_id :string(10) -# name :string(64) -# address :string(255) not null -# kind :integer not null -# nsig :integer -# gateway :string(20) default(""), not null -# settings :string(1000) default({}), not null -# max_balance :decimal(32, 16) default(0.0), not null -# parent :integer -# status :string(32) -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# blockchain_key :string(32) +# currency_id :string(10) +# name :string(64) +# address :string(255) not null +# kind :integer not null +# nsig :integer +# gateway :string(20) default(""), not null +# settings_encrypted :string(1024) +# max_balance :decimal(32, 16) default(0.0), not null +# parent :integer +# status :string(32) +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/bin/init_vault b/bin/init_vault index 243e335a7c..3a316fad69 100755 --- a/bin/init_vault +++ b/bin/init_vault @@ -11,5 +11,6 @@ echo $VAULT_TOKEN docker exec -e "VAULT_TOKEN=${VAULT_TOKEN}" ${DOCKER_VAULT_ID} sh -c \ "vault secrets disable secret \ && vault secrets enable -path=secret -version=1 kv \ + && vault secrets enable transit \ && vault secrets enable totp" diff --git a/config/initializers/vault.rb b/config/initializers/vault.rb index e770c743e6..1ff3b24698 100644 --- a/config/initializers/vault.rb +++ b/config/initializers/vault.rb @@ -2,10 +2,12 @@ # frozen_string_literal: true require 'vault/totp' +require 'vault/rails' Vault.configure do |config| config.address = ENV.fetch('VAULT_URL', 'http://127.0.0.1:8200') config.token = ENV.fetch('VAULT_TOKEN') config.ssl_verify = false config.timeout = 60 + config.application = ENV.fetch('VAULT_APP_NAME', 'peatio') end diff --git a/db/migrate/20190807092706_add_encrypted_secret_to_payment_address.rb b/db/migrate/20190807092706_add_encrypted_secret_to_payment_address.rb new file mode 100644 index 0000000000..112eb38248 --- /dev/null +++ b/db/migrate/20190807092706_add_encrypted_secret_to_payment_address.rb @@ -0,0 +1,67 @@ +class AddEncryptedSecretToPaymentAddress < ActiveRecord::Migration[5.2] + def up + secrets = PaymentAddress.pluck(:id, :secret) + details = PaymentAddress.pluck(:id, :details) + settings = Wallet.pluck(:id, :settings) + + remove_column :payment_addresses, :secret + add_column :payment_addresses, :secret_encrypted , :string, after: :address + + remove_column :payment_addresses, :details + add_column :payment_addresses, :details_encrypted , :string, limit: 1024, after: :secret_encrypted + + remove_column :wallets, :settings + add_column :wallets, :settings_encrypted , :string, limit: 1024, after: :gateway + + secrets.each do |s| + atr = PaymentAddress.__vault_attributes[:secret] + enc = Vault::Rails.encrypt(atr[:path], atr[:key], s[1]) + execute "UPDATE payment_addresses SET #{atr[:encrypted_column]} = '#{enc}' WHERE id = #{s[0]}" + end + + details.each do |d| + atr = PaymentAddress.__vault_attributes[:details] + enc = Vault::Rails.encrypt(atr[:path], atr[:key], d[1]) + execute "UPDATE payment_addresses SET #{atr[:encrypted_column]} = '#{enc}' WHERE id = #{d[0]}" + end + + settings.each do |s| + atr = Wallet.__vault_attributes[:settings] + enc = Vault::Rails.encrypt(atr[:path], atr[:key], s[1]) + execute "UPDATE wallets SET #{atr[:encrypted_column]} = '#{enc}' WHERE id = #{s[0]}" + end + end + + def down + secrets = PaymentAddress.pluck(:id, :secret_encrypted) + details = PaymentAddress.pluck(:id, :details_encrypted) + settings = Wallet.pluck(:id, :settings_encrypted) + + add_column :payment_addresses, :secret, :string, limit: 128, after: :address + remove_column :payment_addresses, :secret_encrypted , :string, after: :address + + add_column :payment_addresses, :details, :string, limit: 1.kilobyte, null: false, default: '{}', after: :secret + remove_column :payment_addresses, :details_encrypted , :string, limit: 1024, after: :secret_encrypted + + add_column :wallets, :settings, :string, limit: 1000, default: '{}', null: false, after: :gateway + remove_column :wallets, :settings_encrypted , :string, limit: 1024, after: :gateway + + secrets.each do |s| + atr = PaymentAddress.__vault_attributes[:secret] + dec = Vault::Rails.decrypt(atr[:path], atr[:key], s[1]) + execute "UPDATE payment_addresses SET secret = '#{dec}' WHERE id = #{s[0]}" + end + + details.each do |d| + atr = PaymentAddress.__vault_attributes[:details] + dec = Vault::Rails.decrypt(atr[:path], atr[:key], d[1]) + execute "UPDATE payment_addresses SET details = '#{dec}' WHERE id = #{d[0]}" + end + + settings.each do |s| + atr = Wallet.__vault_attributes[:settings] + dec = Vault::Rails.decrypt(atr[:path], atr[:key], s[1]) + execute "UPDATE wallets SET settings = '#{dec}' WHERE id = #{s[0]}" + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9f10427aa6..e16d13c62b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_07_26_161540) do +ActiveRecord::Schema.define(version: 2019_08_07_092706) do create_table "accounts", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| t.integer "member_id", null: false @@ -207,8 +207,8 @@ t.string "currency_id", limit: 10, null: false t.integer "account_id", null: false t.string "address", limit: 95 - t.string "secret", limit: 128 - t.string "details", limit: 1024, default: "{}", null: false + t.string "secret_encrypted" + t.string "details_encrypted", limit: 1024 t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["currency_id", "address"], name: "index_payment_addresses_on_currency_id_and_address", unique: true @@ -277,7 +277,7 @@ t.integer "kind", null: false t.integer "nsig" t.string "gateway", limit: 20, default: "", null: false - t.string "settings", limit: 1000, default: "{}", null: false + t.string "settings_encrypted", limit: 1024 t.decimal "max_balance", precision: 32, scale: 16, default: "0.0", null: false t.integer "parent" t.string "status", limit: 32 diff --git a/lib/peatio/vault/totp.rb b/lib/peatio/vault/totp.rb index b07611f495..2c843d19ad 100644 --- a/lib/peatio/vault/totp.rb +++ b/lib/peatio/vault/totp.rb @@ -28,7 +28,7 @@ def with_human_error raise ArgumentError, 'Block is required' unless block_given? yield rescue Vault::VaultError => e - Rails.logger.error { e } + ::Rails.logger.error { e } if e.message.include?('connection refused') raise Error, '2FA server is under maintenance' end diff --git a/spec/models/payment_address_spec.rb b/spec/models/payment_address_spec.rb index 7988429ce0..1d05d0e3de 100644 --- a/spec/models/payment_address_spec.rb +++ b/spec/models/payment_address_spec.rb @@ -5,15 +5,38 @@ context '.create' do let(:member) { create(:member, :level_3) } let!(:account) { member.get_account(:btc) } + let(:secret) { 's3cr3t' } + let(:details) { { 'a' => 'b', 'b' => 'c' } } + let!(:addr) { create(:payment_address, :btc_address, secret: secret) } - after do - DatabaseCleaner.strategy = :truncation - end - - it 'generate address after commit', clean_database_with_truncation: true do + it 'generate address after commit' do AMQPQueue.expects(:enqueue) .with(:deposit_coin_address, { account_id: account.id }, { persistent: true }) account.payment_address end + + it 'updates secret' do + expect { + addr.update(secret: 'new_secret') + }.to change { addr.reload.secret_encrypted }.and change { addr.reload.secret }.to 'new_secret' + end + + it 'updates details' do + expect { + addr.update(details: details) + }.to change { addr.reload.details_encrypted }.and change { addr.reload.details }.to details + end + + it 'long secret' do + expect { + addr.update(secret: Faker::String.random(1024)) + }.to raise_error ActiveRecord::ValueTooLong + end + + it 'long details' do + expect { + addr.update(details: { test: Faker::String.random(1024) }) + }.to raise_error ActiveRecord::ValueTooLong + end end end diff --git a/spec/models/wallet_spec.rb b/spec/models/wallet_spec.rb index 926f5ef35f..fe4e2378d3 100644 --- a/spec/models/wallet_spec.rb +++ b/spec/models/wallet_spec.rb @@ -51,5 +51,33 @@ expect(subject).to_not be_valid expect(subject.errors.full_messages).to eq ['Name has already been taken'] end + + it 'saves settings in encrypted column' do + subject.save + expect { + subject.uri = 'http://geth:8545/' + subject.save + }.to change { subject.settings_encrypted } + end + + it 'does not update settings_encrypted before model is saved' do + subject.save + expect { + subject.uri = 'http://geth:8545/' + }.not_to change { subject.settings_encrypted } + end + + it 'updates setting fields' do + expect { + subject.uri = 'http://geth:8545/' + }.to change { subject.settings['uri'] }.to 'http://geth:8545/' + end + + it 'long encrypted secret' do + expect { + subject.secret = Faker::String.random(1024) + subject.save! + }.to raise_error ActiveRecord::ValueTooLong + end end end