rom_encrypted_attribute 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4fa68f6b6119a75fb160eed6c69ecb341f0966e09b2e1e2d234d0ce20e1f2a1c
4
- data.tar.gz: 4f33329807bc3337d001ebe8c4840ae10ed70272db064343588fc38c444f0452
3
+ metadata.gz: d8f15d21dd70764715c289e7a5c6bc687066517b188b3457a94fc1913f0d09fd
4
+ data.tar.gz: e3b106612840731afffb3ea6fc6df41d142e1f68e5f0f3f349a81b8e89338006
5
5
  SHA512:
6
- metadata.gz: 3ca2ba7ffdb4b96fdc97ca403100bd996593724953a9938483619f82916d62ac7a2c7161e19401fef5cad66837ea6571235dc8064cdb0a7aace0aae3a1776453
7
- data.tar.gz: cc3dc7e8b45d235d5fdf10cd069b84c9ee8a590fcc9247d6df36fedf3529634980c95a32b5978030294cd36ed26858aea4ca4ad97b5debcf1f957dffacfe0957
6
+ metadata.gz: 4a11a62105323e121bad0b230267ad796233dc7638e2f570bf290ec4c427572db0f93c9f20d8ee44e18984bba32a40b6daedbc5c36ea10efc581c8f272172669
7
+ data.tar.gz: 4d74d25778f535512b170cfbce064f0fe38861f4e66fd6b86feda8530e981180ca1fe63db24b57125556e18d61c550f8564cc9038df52908f9932da8e2c42dca
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # RomEncryptedAttribute
2
2
 
3
- This gem adds support for encrypted fields using [ROM](https://rom-rb.org/).
3
+ This gem adds support for encrypted attributes to [ROM](https://rom-rb.org/).
4
4
 
5
- Traditionally ROM suggested to put encryption logic in repository code (more precisely, in the mapper from database struct to an entity). I personally think this is not the greatest idea. Repository lies logically in application layer (or even domain layer), while encryption and decryption of data is a purely infrastructure concern. As such, it should be as low-level and hidden as possible.
5
+ Traditionally ROM team [suggested](https://discourse.rom-rb.org/t/question-encryption-support-thoughts/387) to put encryption logic in repository code (more precisely, in the mapper from database struct to an entity). I personally think this is not the greatest idea. Repository lies logically in the application layer (or even domain layer), while encryption and decryption of data is a purely infrastructure concern. As such, it should be as low-level and hidden as possible.
6
6
 
7
- In ROM terms it means doing it in a relation. This gem uses custom types to achieve encryption and decryption.
7
+ In ROM terms it means doing it in a relation. The gem leverages custom types to achieve encryption and decryption.
8
8
 
9
- The scheme is compatible with Rails' default settings for ActiveRecord encryption, so you can still read encrypted records as long as you provide the same primary key and salt derivation key.
9
+ The scheme is compatible with Rails' default settings for ActiveRecord encryption, so you can still read records encrypted with ActiveRecord from ROM (and vice versa) as long as you provide the same primary key and key derivation salt.
10
10
 
11
11
  ## Installation
12
12
 
@@ -27,7 +27,7 @@ class SecretNotes < ROM::Relation[:sql]
27
27
  EncryptedString, EncryptedStringReader =
28
28
  RomEncryptedAttribute.define_encrypted_attribute_types(
29
29
  primary_key: ENV["ENCRYPTION_PRIMARY_KEY"],
30
- derivation_salt: ENV["ENCRYPTION_DERIVATION_SALT"]
30
+ key_derivation_salt: ENV["ENCRYPTION_KEY_DERIVATION_SALT"]
31
31
  )
32
32
 
33
33
  schema(:secret_notes, infer: true) do
@@ -36,13 +36,29 @@ class SecretNotes < ROM::Relation[:sql]
36
36
  end
37
37
  ```
38
38
 
39
- Of course, you can define it somewhere else and just `include` in the relation or use your custom types code organization.
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.
40
+
41
+ ``` ruby
42
+ class SecretNotes < ROM::Relation[:sql]
43
+ EncryptedString, EncryptedStringReader =
44
+ RomEncryptedAttribute.define_encrypted_attribute_types(
45
+ primary_key: ENV["ENCRYPTION_PRIMARY_KEY"],
46
+ key_derivation_salt: ENV["ENCRYPTION_KEY_DERIVATION_SALT"],
47
+ hash_digest_class: OpenSSL::Digest::SHA256
48
+ )
49
+
50
+ schema(:secret_notes, infer: true) do
51
+ attribute :content, EncryptedString, read: EncryptedStringReader
52
+ end
53
+ end
54
+
55
+ ```
40
56
 
41
57
  ### Caveats
42
58
 
43
- * Due to a bug in `rom-sql`, reading unencrypted data is turned on by default
44
- * The gem uses SHA256 for key derivation and it's currently not configurable
45
- * Support for deterministic encryption from `ActiveRecord::Entryption` is not implemented
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
46
62
 
47
63
  ## Contributing
48
64
 
@@ -2,19 +2,16 @@
2
2
 
3
3
  require "base64"
4
4
  require "json"
5
- require_relative "key_derivator"
5
+ require_relative "payload"
6
6
 
7
7
  module RomEncryptedAttribute
8
8
  class Decryptor
9
- def initialize(secret:, salt:)
10
- @derivator = KeyDerivator.new(secret: secret, salt: salt)
9
+ def initialize(derivator:)
10
+ @derivator = derivator
11
11
  end
12
12
 
13
13
  def decrypt(message)
14
- message = JSON.parse(message)
15
- data = Base64.strict_decode64(message["p"])
16
- iv = Base64.strict_decode64(message["h"]["iv"])
17
- auth_tag = Base64.strict_decode64(message["h"]["at"])
14
+ payload = RomEncryptedAttribute::Payload.decode(message)
18
15
 
19
16
  cipher = OpenSSL::Cipher.new("aes-256-gcm")
20
17
  key = @derivator.derive(cipher.key_len)
@@ -22,10 +19,10 @@ module RomEncryptedAttribute
22
19
  cipher.decrypt
23
20
  cipher.padding = 0
24
21
  cipher.key = key
25
- cipher.iv = iv
26
- cipher.auth_tag = auth_tag
22
+ cipher.iv = payload.initialization_vector
23
+ cipher.auth_tag = payload.auth_tag
27
24
  cipher.auth_data = ""
28
- cipher.update(data) + cipher.final
25
+ cipher.update(payload.message) + cipher.final
29
26
  rescue JSON::ParserError
30
27
  # we need to unconditionally support of reading unencrypted data due to a bug in rom-sql
31
28
  # https://github.com/rom-rb/rom-sql/issues/423
@@ -3,12 +3,12 @@
3
3
  require "base64"
4
4
  require "json"
5
5
  require "openssl"
6
- require_relative "key_derivator"
6
+ require_relative "payload"
7
7
 
8
8
  module RomEncryptedAttribute
9
9
  class Encryptor
10
- def initialize(secret:, salt:)
11
- @derivator = KeyDerivator.new(secret: secret, salt: salt)
10
+ def initialize(derivator:)
11
+ @derivator = derivator
12
12
  end
13
13
 
14
14
  def encrypt(message)
@@ -20,22 +20,7 @@ module RomEncryptedAttribute
20
20
  cipher.key = key
21
21
  cipher.iv = iv
22
22
  encrypted = cipher.update(message) + cipher.final
23
- serialize(encrypted, cipher: cipher, iv: iv)
24
- end
25
-
26
- private
27
-
28
- def serialize(encrypted, iv:, cipher:)
29
- payload =
30
- {
31
- "p" => Base64.strict_encode64(encrypted),
32
- "h" => {
33
- "iv" => Base64.strict_encode64(iv),
34
- "at" => Base64.strict_encode64(cipher.auth_tag)
35
- }
36
- }
37
-
38
- JSON.dump(payload)
23
+ Payload.new(message: encrypted, initialization_vector: iv, auth_tag: cipher.auth_tag).encode
39
24
  end
40
25
  end
41
26
  end
@@ -4,16 +4,17 @@ require "openssl"
4
4
 
5
5
  module RomEncryptedAttribute
6
6
  class KeyDerivator
7
- DIGEST_CLASS = OpenSSL::Digest::SHA256
7
+ DEFAULT_DIGEST_CLASS = OpenSSL::Digest::SHA1
8
8
  ITERATIONS = 2**16
9
9
 
10
- def initialize(secret:, salt:)
10
+ def initialize(secret:, salt:, hash_digest_class: DEFAULT_DIGEST_CLASS)
11
11
  @secret = secret
12
12
  @salt = salt
13
+ @hash_digest_class = hash_digest_class
13
14
  end
14
15
 
15
16
  def derive(size)
16
- OpenSSL::PKCS5.pbkdf2_hmac(@secret, @salt, ITERATIONS, size, DIGEST_CLASS.new)
17
+ OpenSSL::PKCS5.pbkdf2_hmac(@secret, @salt, ITERATIONS, size, @hash_digest_class.new)
17
18
  end
18
19
  end
19
20
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types"
4
+ require "dry/struct"
5
+ require "base64"
6
+
7
+ class RomEncryptedAttribute::Payload < Dry::Struct
8
+ class Types
9
+ include Dry.Types()
10
+ end
11
+
12
+ attribute :message, Types::Strict::String
13
+ attribute :initialization_vector, Types::Strict::String
14
+ attribute :auth_tag, Types::Strict::String
15
+
16
+ def self.decode(database_value)
17
+ payload = JSON.parse(database_value)
18
+ new(
19
+ message: decode64(payload["p"]),
20
+ initialization_vector: decode64(payload.dig("h", "iv")),
21
+ auth_tag: decode64(payload.dig("h", "at"))
22
+ )
23
+ end
24
+
25
+ def self.decode64(value)
26
+ Base64.strict_decode64(value)
27
+ end
28
+
29
+ def encode
30
+ payload =
31
+ {
32
+ "p" => encode64(message),
33
+ "h" => {
34
+ "iv" => encode64(initialization_vector),
35
+ "at" => encode64(auth_tag)
36
+ }
37
+ }
38
+
39
+ JSON.dump(payload)
40
+ end
41
+
42
+ private
43
+
44
+ def encode64(value)
45
+ Base64.strict_encode64(value)
46
+ end
47
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RomEncryptedAttribute
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.3"
5
5
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "rom_encrypted_attribute/key_derivator"
3
4
  require_relative "rom_encrypted_attribute/decryptor"
4
5
  require_relative "rom_encrypted_attribute/encryptor"
5
6
  require_relative "rom_encrypted_attribute/version"
@@ -7,13 +8,15 @@ require_relative "rom_encrypted_attribute/version"
7
8
  require "dry/types"
8
9
 
9
10
  module RomEncryptedAttribute
10
- def self.define_encrypted_attribute_types(primary_key:, derivation_salt:)
11
+ def self.define_encrypted_attribute_types(primary_key:, key_derivation_salt:, hash_digest_class: OpenSSL::Digest::SHA1)
12
+ key_derivator = KeyDerivator.new(salt: key_derivation_salt, secret: primary_key, hash_digest_class: hash_digest_class)
13
+
11
14
  reader_type = Dry.Types.Constructor(String) do |value|
12
- RomEncryptedAttribute::Decryptor.new(secret: primary_key, salt: derivation_salt).decrypt(value)
15
+ RomEncryptedAttribute::Decryptor.new(derivator: key_derivator).decrypt(value)
13
16
  end
14
17
 
15
18
  writer_type = Dry.Types.Constructor(String) do |value|
16
- RomEncryptedAttribute::Encryptor.new(secret: primary_key, salt: derivation_salt).encrypt(value)
19
+ RomEncryptedAttribute::Encryptor.new(derivator: key_derivator).encrypt(value)
17
20
  end
18
21
 
19
22
  [writer_type, reader_type]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rom_encrypted_attribute
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paweł Świątkowski
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-07 00:00:00.000000000 Z
11
+ date: 2024-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-types
@@ -82,6 +82,7 @@ files:
82
82
  - lib/rom_encrypted_attribute/decryptor.rb
83
83
  - lib/rom_encrypted_attribute/encryptor.rb
84
84
  - lib/rom_encrypted_attribute/key_derivator.rb
85
+ - lib/rom_encrypted_attribute/payload.rb
85
86
  - lib/rom_encrypted_attribute/version.rb
86
87
  - rom_encrypted_attribute.gemspec
87
88
  homepage:
@@ -103,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
104
  - !ruby/object:Gem::Version
104
105
  version: '0'
105
106
  requirements: []
106
- rubygems_version: 3.4.21
107
+ rubygems_version: 3.5.6
107
108
  signing_key:
108
109
  specification_version: 4
109
110
  summary: Encrypted attributes for ROM