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 +4 -4
- data/README.md +25 -9
- data/lib/rom_encrypted_attribute/decryptor.rb +7 -10
- data/lib/rom_encrypted_attribute/encryptor.rb +4 -19
- data/lib/rom_encrypted_attribute/key_derivator.rb +4 -3
- data/lib/rom_encrypted_attribute/payload.rb +47 -0
- data/lib/rom_encrypted_attribute/version.rb +1 -1
- data/lib/rom_encrypted_attribute.rb +6 -3
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8f15d21dd70764715c289e7a5c6bc687066517b188b3457a94fc1913f0d09fd
|
4
|
+
data.tar.gz: e3b106612840731afffb3ea6fc6df41d142e1f68e5f0f3f349a81b8e89338006
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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
|
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
|
-
|
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
|
-
|
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
|
44
|
-
*
|
45
|
-
* Support for
|
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 "
|
5
|
+
require_relative "payload"
|
6
6
|
|
7
7
|
module RomEncryptedAttribute
|
8
8
|
class Decryptor
|
9
|
-
def initialize(
|
10
|
-
@derivator =
|
9
|
+
def initialize(derivator:)
|
10
|
+
@derivator = derivator
|
11
11
|
end
|
12
12
|
|
13
13
|
def decrypt(message)
|
14
|
-
|
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 =
|
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(
|
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 "
|
6
|
+
require_relative "payload"
|
7
7
|
|
8
8
|
module RomEncryptedAttribute
|
9
9
|
class Encryptor
|
10
|
-
def initialize(
|
11
|
-
@derivator =
|
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
|
-
|
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
|
-
|
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,
|
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,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:,
|
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(
|
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(
|
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.
|
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:
|
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.
|
107
|
+
rubygems_version: 3.5.6
|
107
108
|
signing_key:
|
108
109
|
specification_version: 4
|
109
110
|
summary: Encrypted attributes for ROM
|