Skip to content

Commit

Permalink
Replace attr_encrypted gem with lockbox
Browse files Browse the repository at this point in the history
  • Loading branch information
StefSchenkelaars committed Oct 26, 2020
1 parent fb102a1 commit cd8259b
Show file tree
Hide file tree
Showing 10 changed files with 46 additions and 42 deletions.
6 changes: 2 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ PATH
keypairs (0.1.0.develop)
actionpack (>= 6.0)
activerecord (>= 6.0)
attr_encrypted (~> 3.1)
jwt (~> 2.1)
lockbox (~> 0.4)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -35,8 +35,6 @@ GEM
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
ast (2.4.1)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
brakeman (4.10.0)
builder (3.2.4)
combustion (1.3.1)
Expand All @@ -50,11 +48,11 @@ GEM
activerecord
database_cleaner (~> 1.8.0)
diff-lcs (1.4.4)
encryptor (3.0.0)
erubi (1.9.0)
i18n (1.8.5)
concurrent-ruby (~> 1.0)
jwt (2.2.2)
lockbox (0.4.9)
loofah (2.7.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ gem 'keypairs'

The of course run `bundle install` and run the migrations `bundle exec rake db:migrate`. The migrations from the gem run automatically.

The private keys are encrypted with the [lockbox](https://github.com/ankane/lockbox) gem. In order for this to work, you need to set the master key as described in [the readme](https://github.com/ankane/lockbox#key-generation), but the easiest thing is to set the environment variable `LOCKBOX_MASTER_KEY` to a sufficient long string (you can generate one with `Lockbox.generate_key`).

## Usage
The central point of this gem is the `Keypair` model which is backed by the `keypairs` table. If you need to sign messages, you can get the current keypair with the `Keypair.current` method. This method performs the rotation of the keypairs if required.

Expand Down
5 changes: 3 additions & 2 deletions app/models/keypair.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require 'attr_encrypted'
require 'lockbox'
require 'jwt'

# This class contains functionality needed for signing messages
Expand All @@ -23,8 +23,9 @@
# @attr [String] jwk_kid The public external id of the key used to find the associated key on decoding.
class Keypair < ActiveRecord::Base
ALGORITHM = 'RS256'
ROTATION_INTERVAL = 1.month

attr_encrypted :_keypair, key: Rails.application.secrets.secret_key_base[0, 32]
encrypts :_keypair

validates :_keypair, presence: true
validates :jwk_kid, presence: true
Expand Down
3 changes: 1 addition & 2 deletions db/migrate/20201024100500_create_keypairs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ class CreateKeypairs < ActiveRecord::Migration[6.0]
def change
create_table :keypairs do |t|
t.string :jwk_kid, null: false
t.string :encrypted__keypair, null: false
t.string :encrypted__keypair_iv, null: false
t.text :_keypair_ciphertext, null: false
t.timestamps precision: 6
# Since we are ordering on created_at, let's create an index
t.index :created_at
Expand Down
3 changes: 1 addition & 2 deletions keypairs.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ Gem::Specification.new do |spec|

spec.add_dependency 'actionpack', '>= 6.0' # Depend on actionpack to share public keys
spec.add_dependency 'activerecord', '>= 6.0' # Depend on activerecord as ORM
# TODO: Replace with https://github.com/ankane/lockbox
spec.add_dependency 'attr_encrypted', '~> 3.1' # Encrypt and decrypt attributes
spec.add_dependency 'jwt', '~> 2.1' # Working with JSON Web Tokens
spec.add_dependency 'lockbox', '~> 0.4' # Encrypt and decrypt attributes

spec.add_development_dependency 'brakeman' # Static analysis security vulnerability scanner
spec.add_development_dependency 'combustion' # Test rails engines
Expand Down
1 change: 1 addition & 0 deletions lib/keypairs.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# frozen_string_literal: true

require 'lockbox'
require 'keypairs/engine'
5 changes: 2 additions & 3 deletions spec/models/keypair_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
describe 'database' do
it { is_expected.to have_db_column(:id).of_type(:integer).with_options(null: false) }
it { is_expected.to have_db_column(:jwk_kid).of_type(:string).with_options(null: false) }
it { is_expected.to have_db_column(:encrypted__keypair).of_type(:string).with_options(null: false) }
it { is_expected.to have_db_column(:encrypted__keypair_iv).of_type(:string).with_options(null: false) }
it { is_expected.to have_db_column(:_keypair_ciphertext).of_type(:text).with_options(null: false) }
it { is_expected.to have_db_column(:created_at).of_type(:datetime).with_options(null: false, precision: 6) }
it { is_expected.to have_db_column(:updated_at).of_type(:datetime).with_options(null: false, precision: 6) }
it { is_expected.to have_db_index(:created_at) }
Expand All @@ -15,7 +14,7 @@
describe 'settings' do
it { expect(described_class::ALGORITHM).to eq 'RS256' }
it { is_expected.to be_a(ActiveRecord::Base) }
it { is_expected.to have_attr_encrypted(:_keypair) }
it { is_expected.to encrypt_attribute(:_keypair) }
end

describe 'validations' do
Expand Down
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

require 'keypairs'

Lockbox.master_key = Lockbox.generate_key

RSpec.configure do |config|
config.use_transactional_fixtures = true

Expand Down
32 changes: 32 additions & 0 deletions spec/support/matchers/encrypt_attribute.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

RSpec::Matchers.define :encrypt_attribute do |attribute|
database_column_name = "#{attribute}_ciphertext"

match do |model|
# Correct responds to methods
model.respond_to?(attribute) &&
model.respond_to?("#{attribute}=") &&
model.respond_to?(database_column_name) &&
model.respond_to?("#{database_column_name}=") &&
# Correct database columns
model.class.column_names.exclude?(attribute.to_s) &&
model.class.column_names.include?(database_column_name)
end

failure_message do |model|
if model.class.column_names.include?(database_column_name)
"#{attribute} should use lockbox encrypts on #{model.class}"
else
"#{database_column_name} must be a column on #{model.class} for encryption to work"
end
end

failure_message_when_negated do |model|
if model.class.column_names.include?(database_column_name)
"#{attribute} should not use lockbox encrypts on #{model.class}"
else
"#{database_column_name} shouldn't be a column on #{model.class}"
end
end
end
29 changes: 0 additions & 29 deletions spec/support/matchers/have_attr_encrypted.rb

This file was deleted.

0 comments on commit cd8259b

Please sign in to comment.