rom-encrypted_attribute 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26555bd77c9580cd7889520b0cd9737065ac1be7b66572770ddae63cd4ffe5df
4
- data.tar.gz: 5a7b73206a06cadec42c6ad584ad239a3f81bcb9e30790641d4bb1868fd75d22
3
+ metadata.gz: edac4dc1b09b3b903aefc265d78a56a7fafd2f6d28f1b503d3f4fbe64eede9c6
4
+ data.tar.gz: c7de7fd6c2bc5c6de81ef0bf3f804f7cda02df4d5687156647155dd77e5c358e
5
5
  SHA512:
6
- metadata.gz: cf70faabb89dad5e071bae34ad030c87816656a27384c3aed6f4247f8d7d5eef9cb67ce365ec6d5e62a0e4341cc604ab8169e2a6fb641a31fbe71ffa7d02a1ae
7
- data.tar.gz: 335fe66bfa7993e3a4dd22f2f40b86fd0e6ab85ec94e288804f3abad89a166036da8a62351b495f111437ea7084b46ad32b25d4ae86ab7ecd734fc80c3cf7784
6
+ metadata.gz: d603a67e727a2079904ed7f45629fcc2fabe185297ea26f479b9573c8d8b1bb366c6a404ca3720176769890b70d280490cb198b7798731b8efbbe92a47d68581
7
+ data.tar.gz: dd4d03515ab81bbd335793688f202e73b21f9ad815d9881700e8cf99ff6dc69bc95aebe8c15a74440283b80c19c7c17850133770b98a06a19df54e61476f568f
data/README.md CHANGED
@@ -20,6 +20,60 @@ If bundler is not being used to manage dependencies, install the gem by executin
20
20
 
21
21
  ## Usage
22
22
 
23
+ There are two ways to use this library: via a ROM schema plugin or by a "bare metal" approach.
24
+
25
+ ### ROM plugin
26
+
27
+ Somewhere in you code set the config for the gem. This is done using [`Dry::Configurable`](https://dry-rb.org/gems/dry-configurable/1.0/), so you can use all options available there. For example:
28
+
29
+ ``` ruby
30
+ ROM::EncryptedAttribute.configure do |config|
31
+ config.primary_key = "your-primary-key" # required
32
+ config.key_derivation_salt = "your-derivation-salt" # required
33
+ config.hash_digest_class = OpenSSL::Digest::SHA256 # SHA1 by default
34
+ end
35
+ ```
36
+
37
+ Then use the plugin in your ROM relation:
38
+
39
+ ``` ruby
40
+ class SecretNotes < ::ROM::Relation[:sql]
41
+ schema(:secret_notes, infer: true) do
42
+ use :encrypted_attributes
43
+ encrypt :content
44
+ end
45
+ end
46
+ ```
47
+
48
+ You can override individual configuration values if, for example, one database table uses different primary key:
49
+
50
+ ``` ruby
51
+ class SecretNotes < ::ROM::Relation[:sql]
52
+ schema(:secret_notes, infer: true) do
53
+ use :encrypted_attributes, primary_key: ENV["SPECIAL_PRIMARY_KEY"]
54
+ encrypt :content
55
+ end
56
+ end
57
+ ```
58
+
59
+ If you specify all configuration options, or use defaults, you can skip setting the global config.
60
+
61
+ You can also override global per-schema settings on a per-field level:
62
+
63
+ ``` ruby
64
+ class SecretNotes < ::ROM::Relation[:sql]
65
+ schema(:secret_notes, infer: true) do
66
+ use :encrypted_attributes
67
+ encrypt :content
68
+ encrypt :title, :hash_digest_class: OpenSSL::Digest::SHA256
69
+ encrypt :maybe_encrypted, support_unencrypted_data: true
70
+ end
71
+ end
72
+
73
+ ```
74
+
75
+ ### "Bare metal"
76
+
23
77
  In your relation, define custom types using a helper method from the gem. You need to provide the credentials to it somehow. This might be done via environmental variables, Hanami settings (if you're using Hanami) or any other means, really.
24
78
 
25
79
  ```ruby
@@ -36,7 +90,11 @@ class SecretNotes < ROM::Relation[:sql]
36
90
  end
37
91
  ```
38
92
 
39
- By default the gem uses SHA1 for key derivation (same as Rails' default), but you can configure it by passing custom `has_digest_class` option.
93
+ With this approach you can define the types globally in your application and reuse it, without having to pass primary_key and key_derivation_salt every time in the schema.
94
+
95
+ ### Other considerations
96
+
97
+ By default the gem uses SHA1 for key derivation (same as Rails' default), but you can configure it by passing custom `hash_digest_class` option.
40
98
 
41
99
  ``` ruby
42
100
  class SecretNotes < ROM::Relation[:sql]
@@ -56,9 +114,9 @@ end
56
114
 
57
115
  ### Caveats
58
116
 
59
- * Due to [a bug](https://github.com/rom-rb/rom-sql/issues/423) in `rom-sql`, reading unencrypted data is always supported, which means that if there's a plain not-encrypted data in your database already, it will be read correctly. This might or might not be desirable, but for the time being there's no choice in cofiguring this behaviour.
60
- * Support for deterministic encryption from `ActiveRecord::Encryption` is not implemented
61
- * Support for key rotation is not implemented
117
+ * Due to [a bug](https://github.com/rom-rb/rom-sql/issues/423) in `rom-sql`, reading unencrypted data requires a monkey patch to ROM. Since this is quite aggressive, you are expected to opt-in to this by calling `ROM::SQL::Patch432.install!`.
118
+ * Support for deterministic encryption from `ActiveRecord::Encryption` is not (yet) implemented
119
+ * Support for key rotation is not (yet) implemented
62
120
 
63
121
  ## Contributing
64
122
 
@@ -7,11 +7,16 @@ require_relative "payload"
7
7
  module ROM
8
8
  module EncryptedAttribute
9
9
  class Decryptor
10
- def initialize(derivator:)
10
+ UnencryptedDataNotAllowed = Class.new(RuntimeError)
11
+
12
+ def initialize(derivator:, support_unencrypted_data: false)
11
13
  @derivator = derivator
14
+ @support_unencrypted_data = support_unencrypted_data
12
15
  end
13
16
 
14
17
  def decrypt(message)
18
+ return nil if message.nil?
19
+
15
20
  payload = ROM::EncryptedAttribute::Payload.decode(message)
16
21
 
17
22
  cipher = OpenSSL::Cipher.new("aes-256-gcm")
@@ -25,9 +30,11 @@ module ROM
25
30
  cipher.auth_data = ""
26
31
  cipher.update(payload.message) + cipher.final
27
32
  rescue JSON::ParserError
28
- # we need to unconditionally support of reading unencrypted data due to a bug in rom-sql
29
- # https://github.com/rom-rb/rom-sql/issues/423
30
- message
33
+ if @support_unencrypted_data
34
+ message
35
+ else
36
+ raise UnencryptedDataNotAllowed
37
+ end
31
38
  end
32
39
  end
33
40
  end
@@ -13,6 +13,8 @@ module ROM
13
13
  end
14
14
 
15
15
  def encrypt(message)
16
+ return nil if message.nil?
17
+
16
18
  cipher = OpenSSL::Cipher.new("aes-256-gcm")
17
19
  key = @derivator.derive(cipher.key_len)
18
20
  iv = cipher.random_iv
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this is used to silence the redefinition warnings
4
+ require "warning"
5
+
6
+ # This provides a patch for rom-sql while https://github.com/rom-rb/rom-sql/pull/432
7
+ # is pending merge. It allows to block reading unencrypted values from the database.
8
+ # You need to opt-in for this patch yourself by calling:
9
+ #
10
+ # ROM::SQL::Patch432.install!
11
+ #
12
+ # The patch can be reverted with
13
+ # ROM::SQL::Patch432.uninstall!
14
+ #
15
+ # Note: that you don't need this if you are using PostgreSQL.
16
+ # Also note that this will emit a warning of overwriting a method.
17
+ module ROM
18
+ module SQL
19
+ module Patch432
20
+ def self.install!
21
+ Warning.ignore(:method_redefined)
22
+ ROM::SQL::Commands::Create.class_eval do
23
+ def insert(tuples)
24
+ pks = tuples.map { |tuple| relation.insert(tuple) }
25
+ relation.dataset.where(relation.primary_key => pks).to_a
26
+ end
27
+
28
+ def multi_insert(tuples)
29
+ pks = relation.multi_insert(tuples, return: :primary_key)
30
+ relation.dataset.where(relation.primary_key => pks).to_a
31
+ end
32
+ end
33
+ Warning.ignore(:method_redefined, false)
34
+ end
35
+
36
+ def self.uninstall!
37
+ Warning.ignore(:method_redefined)
38
+ ROM::SQL::Commands::Create.class_eval do
39
+ def insert(tuples)
40
+ pks = tuples.map { |tuple| relation.insert(tuple) }
41
+ relation.where(relation.primary_key => pks).to_a
42
+ end
43
+
44
+ def multi_insert(tuples)
45
+ pks = relation.multi_insert(tuples, return: :primary_key)
46
+ relation.where(relation.primary_key => pks).to_a
47
+ end
48
+ end
49
+ Warning.ignore(:method_redefined, false)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ROM
4
4
  module EncryptedAttribute
5
- VERSION = "0.0.3"
5
+ VERSION = "0.0.5"
6
6
  end
7
7
  end
@@ -4,20 +4,32 @@ require_relative "encrypted_attribute/key_derivator"
4
4
  require_relative "encrypted_attribute/decryptor"
5
5
  require_relative "encrypted_attribute/encryptor"
6
6
  require_relative "encrypted_attribute/version"
7
+ require_relative "plugins/schema/encrypted_attributes"
7
8
 
8
9
  require "dry/types"
9
10
 
10
11
  module ROM
11
12
  module EncryptedAttribute
12
- def self.define_encrypted_attribute_types(primary_key:, key_derivation_salt:, hash_digest_class: OpenSSL::Digest::SHA1)
13
+ extend Dry::Configurable
14
+
15
+ module Types
16
+ include Dry.Types()
17
+ end
18
+
19
+ setting :primary_key
20
+ setting :key_derivation_salt
21
+ setting :hash_digest_class, default: OpenSSL::Digest::SHA1
22
+ setting :support_unencrypted_data, default: false
23
+
24
+ def self.define_encrypted_attribute_types(primary_key:, key_derivation_salt:, hash_digest_class: OpenSSL::Digest::SHA1, support_unencrypted_data: false)
13
25
  key_derivator = KeyDerivator.new(salt: key_derivation_salt, secret: primary_key,
14
26
  hash_digest_class: hash_digest_class)
15
27
 
16
- reader_type = Dry.Types.Constructor(String) do |value|
17
- ROM::EncryptedAttribute::Decryptor.new(derivator: key_derivator).decrypt(value)
28
+ reader_type = Dry.Types.Constructor(Types::String.optional) do |value|
29
+ ROM::EncryptedAttribute::Decryptor.new(derivator: key_derivator, support_unencrypted_data: support_unencrypted_data).decrypt(value)
18
30
  end
19
31
 
20
- writer_type = Dry.Types.Constructor(String) do |value|
32
+ writer_type = Dry.Types.Constructor(Types::String.optional) do |value|
21
33
  ROM::EncryptedAttribute::Encryptor.new(derivator: key_derivator).encrypt(value)
22
34
  end
23
35
 
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom"
4
+
5
+ module ROM
6
+ module Plugins
7
+ module Schema
8
+ module EncryptedAttributes
9
+ def self.apply(schema, **options)
10
+ attributes = options.fetch(:attributes)
11
+ primary_key = options.fetch(:primary_key, ROM::EncryptedAttribute.config.primary_key)
12
+ key_derivation_salt = options.fetch(:key_derivation_salt, ROM::EncryptedAttribute.config.key_derivation_salt)
13
+ hash_digest_class = options.fetch(:hash_digest_class, ROM::EncryptedAttribute.config.hash_digest_class)
14
+ support_unencrypted_data = options.fetch(:support_unencrypted_data, EncryptedAttribute.config.support_unencrypted_data)
15
+
16
+ encrypted_string, encrypted_string_reader =
17
+ ROM::EncryptedAttribute.define_encrypted_attribute_types(
18
+ primary_key: primary_key, key_derivation_salt: key_derivation_salt, hash_digest_class: hash_digest_class, support_unencrypted_data: support_unencrypted_data
19
+ )
20
+
21
+ attrs =
22
+ attributes.map do |name|
23
+ ROM::Schema::DSL.new(schema.name).build_attribute_info(
24
+ name,
25
+ encrypted_string,
26
+ name: name, read: encrypted_string_reader
27
+ )
28
+ end
29
+
30
+ schema.attributes.concat(
31
+ schema.class.attributes(attrs, schema.attr_class)
32
+ )
33
+ end
34
+
35
+ module DSL
36
+ # @example
37
+ # schema do
38
+ # use :encrypted_attributes
39
+ # encrypt :api_key, :ssn, hash_digest_class: OpenSSL::Digest::SHA256
40
+ # end
41
+ def encrypt(*attributes, **opts)
42
+ options = plugin_options(:encrypted_attributes)
43
+ options.merge!(opts)
44
+ options[:attributes] ||= []
45
+ options[:attributes] += attributes
46
+ self
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ ROM.plugins do
55
+ register :encrypted_attributes, ROM::Plugins::Schema::EncryptedAttributes, type: :schema
56
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rom-encrypted_attribute
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paweł Świątkowski
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-09-21 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: dry-types
@@ -66,7 +65,6 @@ dependencies:
66
65
  - - ">="
67
66
  - !ruby/object:Gem::Version
68
67
  version: '1.0'
69
- description:
70
68
  email:
71
69
  - katafrakt@vivaldi.net
72
70
  executables: []
@@ -83,13 +81,13 @@ files:
83
81
  - lib/rom/encrypted_attribute/encryptor.rb
84
82
  - lib/rom/encrypted_attribute/key_derivator.rb
85
83
  - lib/rom/encrypted_attribute/payload.rb
84
+ - lib/rom/encrypted_attribute/rom_sql_patch.rb
86
85
  - lib/rom/encrypted_attribute/version.rb
86
+ - lib/rom/plugins/schema/encrypted_attributes.rb
87
87
  - rom-encrypted_attribute.gemspec
88
- homepage:
89
88
  licenses: []
90
89
  metadata:
91
90
  source_code_uri: https://github.com/katafrakt/rom-encrypted_attribute
92
- post_install_message:
93
91
  rdoc_options: []
94
92
  require_paths:
95
93
  - lib
@@ -104,8 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
102
  - !ruby/object:Gem::Version
105
103
  version: '0'
106
104
  requirements: []
107
- rubygems_version: 3.5.16
108
- signing_key:
105
+ rubygems_version: 3.6.7
109
106
  specification_version: 4
110
107
  summary: Encrypted attributes for ROM
111
108
  test_files: []