easy_crypt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a3936f18965419a36551cd0cc5b5e4d8acd6bdd3afe1ba39dc9879869fb1f649
4
+ data.tar.gz: 852d38340c89b75221b884eb9e12d2475f06e441e19abad17e9f5ca216a13024
5
+ SHA512:
6
+ metadata.gz: 3b84dd63f38fe9b6802a5ca667f38800b346e8b52040147967872db726f2e4e23de37702259a5744fc421b1c228deab46757d7b8b8966af9e4724b8dd7c50007
7
+ data.tar.gz: d3769217d764e084b035785d6a7d7a1e52df286ad1de46a374d3ca115d36f1d1f110c6e75d040a5ed0dd196f5e7ee940b699578778ac927dce538e950070afac
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## [1.0.0] - 2025-01-27
2
+
3
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Benjamin Bouchet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # EasyCrypt
2
+
3
+ EasyCrypt is a Ruby utility that provides secure and flexible encryption and decryption capabilities for Ruby on Rails applications. It is built on top of Rails’ ActiveSupport::MessageEncryptor, allowing you to securely and easily encrypt and decrypt data.
4
+
5
+ ## Features
6
+
7
+ - Multiple secrets providers support (currently _Rails credentials_ and _env variables_).
8
+ - Simple, purpose-based encryption/decryption API.
9
+ - Configurable encryption cipher.
10
+ - Minimal configuration required.
11
+ - Built-in encryption signatures to ensure data integrity.
12
+
13
+ ## Table of Contents
14
+
15
+ - [Installation](#installation)
16
+ - [Configuration](#configuration)
17
+ - [Usage](#usage)
18
+ - [Secrets Provider Configuration](#secrets-provider-configuration)
19
+ - [Encrypting Data](#encrypting-data)
20
+ - [Decrypting Data](#decrypting-data)
21
+ - [Dynamic Method Calls](#dynamic-method-calls)
22
+ - [Error Handling](#error-handling)
23
+ - [Customizing the Cipher](#customizing-the-cipher)
24
+ - [Advanced Topics](#advanced-topics)
25
+ - [Message Expiration](#message-expiration)
26
+ - [Performance Considerations](#performance-considerations)
27
+ - [Security Best Practices](#security-best-practices)
28
+ - [Contributing](#contributing)
29
+ - [License](#license)
30
+ - [Code of Conduct](#code-of-Conduct)
31
+
32
+ ## Installation
33
+
34
+ Add this line to your application's Gemfile:
35
+
36
+ ```ruby
37
+ gem 'easy_crypt'
38
+ ```
39
+
40
+ Then execute:
41
+
42
+ ```ruby
43
+ bundle install
44
+ ```
45
+
46
+ Or install it yourself:
47
+
48
+ ```ruby
49
+ gem install easy_crypt
50
+ ```
51
+
52
+ ## Configuration
53
+
54
+ Create an initializer in your Rails application (e.g., `config/initializers/easy_crypt.rb`).
55
+ Configure EasyCrypt to specify how secrets are stored and which cipher to use:
56
+
57
+ ```ruby
58
+ EasyCrypt.configure do |config|
59
+ # Choose your secrets provider. Currently supports :rails_credentials or :env_vars
60
+ # Default is :rails_credentials
61
+ config.secrets_provider = :rails_credentials
62
+
63
+ # Set your default cipher
64
+ # Default is identical to ActiveSupport::MessageEncryptor.default_cipher,
65
+ # commonly 'aes-256-gcm' or 'aes-256-cbc', depending on your Rails configuration
66
+ config.default_cipher = 'aes-256-gcm'
67
+ end
68
+ ```
69
+
70
+ ## Usage
71
+
72
+ EasyCrypt expects you to define secrets for multiple "purposes". This means that if you’re encrypting user data, you can set up one pair of secret and salt for the "user_data" purpose. If you’re encrypting tokens for session authentication, you might use the "authentication" purpose with a different pair of secret and salt, and so on.
73
+
74
+ ### Secrets Provider Configuration
75
+
76
+ EasyCrypt currently supports two types of secrets providers: environment variables (`:env_vars`) and Rails credentials (`:rails_credentials`).
77
+
78
+ #### Using Rails Credentials
79
+
80
+ With Rails credentials, you can define secrets for each purpose in `config/credentials.yml.enc`:
81
+
82
+ ```yaml
83
+ easy_crypt:
84
+ authentication:
85
+ salt: "auth-salt"
86
+ secret: "auth-secret"
87
+ user_data:
88
+ salt: "user-data-salt"
89
+ secret: "user-data-secret"
90
+ ```
91
+
92
+ This structure allows you to define multiple purposes under the `easy_crypt` key, each with its own `salt` and `secret`. EasyCrypt will automatically retrieve the corresponding values when encrypting or decrypting using the corresponding purpose.
93
+
94
+ The example above defines "authentication" and "user_data" purpose, but you are free to pick the names that make sense for the purposes you need, as long as you follow the same structure.
95
+
96
+ #### Using Environment Variables
97
+
98
+ If you prefer using environment variables, define the salt and secret for each purpose in your `.env` file or directly in the environment. The format is:
99
+
100
+ ```env
101
+ EASY_CRYPT_<PURPOSE>_SALT=your-salt
102
+ EASY_CRYPT_<PURPOSE>_SECRET=your-secret
103
+ ```
104
+
105
+ For example:
106
+
107
+ ```env
108
+ EASY_CRYPT_AUTHENTICATION_SALT=auth-salt
109
+ EASY_CRYPT_AUTHENTICATION_SECRET=auth-secret
110
+ EASY_CRYPT_USER_DATA_SALT=user-data-salt
111
+ EASY_CRYPT_USER_DATA_SECRET=user-data-secret
112
+ ```
113
+
114
+ You can then encrypt or decrypt data for these purposes.
115
+
116
+ ### Encrypting Data
117
+
118
+ To encrypt a piece of data with the "user_data" purpose:
119
+
120
+ ```ruby
121
+ encrypted_value = EasyCrypt.encrypt_user_data("Sensitive Information")
122
+ ```
123
+
124
+ Internally, EasyCrypt automatically retrieves the secret and salt for the "user_data" purpose, using your configured secrets provider. It uses `ActiveSupport::MessageEncryptor` to encrypt and sign the data.
125
+
126
+ ### Decrypting Data
127
+
128
+ To decrypt the data you just encrypted, call:
129
+
130
+ ```ruby
131
+ decrypted_value = EasyCrypt.decrypt_user_data(encrypted_value)
132
+
133
+ if decrypted_value
134
+ puts "Decrypted successfully: #{decrypted_value}"
135
+ else
136
+ puts "Invalid or expired data."
137
+ end
138
+ ```
139
+
140
+ If anything goes wrong in the decryption process (e.g., invalid signature or message expiration when using time-based encryption), EasyCrypt returns `nil` rather than raising an exception.
141
+
142
+ ### Dynamic Method Calls
143
+
144
+ EasyCrypt uses `method_missing` to allow dynamic encrypt/decrypt methods for different purposes, following this pattern:
145
+
146
+ ```ruby
147
+ encrypt_<purpose>
148
+ decrypt_<purpose>
149
+ ```
150
+
151
+ For example, if you want data to be encrypted with a "passwords" purpose:
152
+
153
+ ```ruby
154
+ encrypted_pw = EasyCrypt.encrypt_passwords("super_secret")
155
+ decrypted_pw = EasyCrypt.decrypt_passwords(encrypted_pw)
156
+ ```
157
+
158
+ Given that you have properly defined the secret and salt for the "passwords" purpose in your secrets provider.
159
+
160
+ ### Error Handling
161
+
162
+ When decryption fails (e.g., altered data or expired message), EasyCrypt returns `nil` to avoid exposing decryption errors.
163
+
164
+ For method names that don't match the `encrypt_<purpose>` or `decrypt_<purpose>` naming pattern, a `NoMethodError` will be raised.
165
+
166
+ If your secrets provider returns invalid or missing credentials, such as when `salt` or `secret` are missing, EasyCrypt raises a `MissingSecretsConfigurationError`.
167
+
168
+ ### Customizing the Cipher
169
+
170
+ EasyCrypt uses by default the cipher returned by `ActiveSupport::MessageEncryptor.default_cipher`, which depends on your application configuration.
171
+
172
+ You can override the gem’s default cipher through EasyCrypt configuration:
173
+
174
+ ```ruby
175
+ EasyCrypt.configure do |config|
176
+ # Choose a different cipher if desired
177
+ config.default_cipher = 'aes-128-gcm'
178
+ end
179
+ ```
180
+
181
+ Secrets Providers can also specify cipher for a particular purpose.
182
+
183
+ #### Using Rails Credentials
184
+
185
+ In `config/credentials.yml.enc`:
186
+
187
+ ```yaml
188
+ easy_crypt:
189
+ data_in_transit:
190
+ cipher: "aes-128-gcm"
191
+ salt: "auth-salt"
192
+ secret: "auth-secret"
193
+ ```
194
+
195
+ Here, the `data_in_transit` purpose will use the `aes-128-gcm` cipher, overriding the default cipher.
196
+
197
+ ### Using Environment Variables
198
+
199
+ You can set the cipher for a purpose using an environment variable:
200
+
201
+ ```env
202
+ EASY_CRYPT_DATA_IN_TRANSIT_CIPHER=aes-128-gcm
203
+ EASY_CRYPT_DATA_IN_TRANSIT_SALT=auth-salt
204
+ EASY_CRYPT_DATA_IN_TRANSIT_SECRET=auth-secret
205
+ ```
206
+
207
+ Here, the `data_in_transit` purpose will use the `aes-128-gcm` cipher, overriding the default cipher.
208
+
209
+ ## Advanced Topics
210
+
211
+ ### Message Expiration
212
+
213
+ EasyCrypt supports passing `expires_at` or `expires_in` to the underlying `encrypt_and_sign` method as described in [Rails' documentation](ActiveSupport::MessageEncryptor.default_cipher). If provided, the data becomes invalid after that time:
214
+
215
+ ```ruby
216
+ encrypted = EasyCrypt.encrypt_user_data("Sensitive Info", expires_in: 1.hour)
217
+ ```
218
+
219
+ ### Performance Considerations
220
+
221
+ If you need to handle large data encryption, consider compressing it before calling the encrypt methods. Also, avoid redundant encryption in tight loops for better performance.
222
+
223
+ ### Security Best Practices
224
+
225
+ This list is not exhaustive:
226
+
227
+ - __Use Strong Algorithms__: Choose encryption methods wisely, AES-256 for symmetric and RSA-2048+ or elliptic curves for asymmetric encryption.
228
+ - __Hashing for Validation__: Use secure hashing algorithms (e.g., bcrypt, Argon2) to validate secrets you don't need to store, like passwords.
229
+ - __Sign for Integrity__: Sign data to prevent tampering (e.g., with RSA or ECDSA).
230
+ - __Store Secrets Securely__: Use secret management tools (e.g., HashiCorp Vault).
231
+ - __Rotate Secrets Periodically__: Regularly update keys and credentials to limit exposure.
232
+ - __Avoid Exposing Secrets__: Never log sensitive information or hardcode secrets in code or repositories.
233
+ - __Encrypt Everywhere__: Encrypt data at rest, in transit, and even on trusted networks.
234
+ - __Rely on Proven Libraries__: Avoid building custom encryption; use trusted libraries (e.g., OpenSSL, EasyCrypt). Note: EasyCrypt uses OpenSSL under the hood.
235
+
236
+ ## Contributing
237
+
238
+ Bug reports and pull requests are welcome on GitHub at https://github.com/randoum/easy_crypt. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/randoum/easy_crypt/blob/main/CODE_OF_CONDUCT.md).
239
+
240
+ Adding a rotation feature could be a good contribution, if you feel like doing so.
241
+
242
+ ## License
243
+
244
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
245
+
246
+ ## Code of Conduct
247
+
248
+ Everyone interacting in the EasyCrypt project's codebases and issue trackers is expected to follow the [code of conduct](https://github.com/randoum/easy_crypt/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyCrypt
4
+ # Exception raised when the requested secrets provider does not exist
5
+ class InvalidSecretsProvider < StandardError
6
+ def initialize(str)
7
+ super("Unknown secrets provider `#{str}`")
8
+ end
9
+ end
10
+
11
+ # Exception raised when secrets are missing for the requested purpose
12
+ class MissingSecretsConfigurationError < StandardError
13
+ def initialize(purpose, missing_attributes)
14
+ super("Missing required secret configuration for '#{purpose}': #{missing_attributes.join(', ')}")
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyCrypt
4
+ class SecretsProvider
5
+ # Base class for all secret providers. Implements the interface that
6
+ # concrete providers must follow.
7
+ #
8
+ # This abstract base class defines the structure for secrets providers, including
9
+ # required methods for retrieving encryption configuration values such as `salt`,
10
+ # `secret`, and `cipher`. Subclasses are expected to implement these methods based
11
+ # on their respective sources (e.g., environment variables, Rails credentials).
12
+ #
13
+ # @abstract Subclass and override {#secret}, {#salt}, and {#cipher} to implement
14
+ # a custom secrets provider.
15
+ class Base
16
+ # @return [Symbol] The purpose of the credential being accessed
17
+ attr_reader :purpose
18
+
19
+ # Initialize a new secrets provider
20
+ #
21
+ # @param purpose [Symbol] The purpose of the credential (e.g., :authentication)
22
+ def initialize(purpose)
23
+ @purpose = purpose
24
+ end
25
+
26
+ # Validates the presence of required attributes for the secrets provider.
27
+ #
28
+ # Checks if the attributes `salt`, and `secret` are defined.
29
+ # Raises a `MissingSecretsConfigurationError` error if any of these attributes are missing.
30
+ #
31
+ # @raise [MissingSecretsConfigurationError] If any required attribute is missing.
32
+ def validate_attributes!
33
+ missing_attributes = %w[salt secret].reject { |attr| send(attr) }
34
+ raise MissingSecretsConfigurationError.new(purpose, missing_attributes) if missing_attributes.any?
35
+ end
36
+
37
+ # Returns the encryption cipher.
38
+ #
39
+ # @abstract
40
+ # @return [String, nil] The encryption cipher.
41
+ # @raise [NotImplementedError] If not implemented in the subclass.
42
+ def cipher
43
+ raise NotImplementedError, 'Subclasses must implement #cipher'
44
+ end
45
+
46
+ # Returns the salt value for key generation
47
+ #
48
+ # @abstract
49
+ # @return [String] The salt value
50
+ # @raise [NotImplementedError] If the subclass doesn't implement this method
51
+ def salt
52
+ raise NotImplementedError, 'Subclasses must implement #salt'
53
+ end
54
+
55
+ # Returns the secret key for encryption
56
+ #
57
+ # @abstract
58
+ # @return [String] The secret key
59
+ # @raise [NotImplementedError] If the subclass doesn't implement this method
60
+ def secret
61
+ raise NotImplementedError, 'Subclasses must implement #secret'
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyCrypt
4
+ class SecretsProvider
5
+ # Provider that fetches encryption configuration from environment variables
6
+ #
7
+ # Multiple credential purposes can be configured using environment variables.
8
+ # Each credential purpose requires three environment variables following this pattern:
9
+ #
10
+ # EASY_CRYPT_[PURPOSE]_CIPHER - (optional) Cipher to use for the encryption
11
+ # falls back to EasyCrypt.default_cipher is not set
12
+ # EASY_CRYPT_[PURPOSE]_SALT - Salt value for the encryption
13
+ # EASY_CRYPT_[PURPOSE]_SECRET - Secret key for the encryption
14
+ #
15
+ # Where [PURPOSE] is the uppercase version of the credential purpose
16
+ #
17
+ # @example Using authentication credentials
18
+ # # In .env file or environment:
19
+ # EASY_CRYPT_AUTHENTICATION_CIPHER=aes128
20
+ # EASY_CRYPT_AUTHENTICATION_SALT=your-salt-value
21
+ # EASY_CRYPT_AUTHENTICATION_SECRET=your-secret-key
22
+ #
23
+ # provider = EnvProvider.new(:authentication)
24
+ # provider.cipher #=> "aes128"
25
+ # provider.salt #=> "your-salt-value"
26
+ # provider.secret #=> "your-secret-key"
27
+ #
28
+ # @example Using user_data credentials
29
+ # # In .env file or environment:
30
+ # EASY_CRYPT_USER_DATA_SALT=different-salt
31
+ # EASY_CRYPT_USER_DATA_SECRET=different-secret
32
+ #
33
+ # provider = EnvProvider.new(:user_data)
34
+ # provider.cipher #=> nil
35
+ # provider.salt #=> "different-salt"
36
+ # provider.secret #=> "different-secret"
37
+ #
38
+ # You can set these variables in your .env file:
39
+ # echo "EASY_CRYPT_AUTHENTICATION_CIPHER=aes128" >> .env
40
+ # echo "EASY_CRYPT_AUTHENTICATION_SALT=your-salt" >> .env
41
+ # echo "EASY_CRYPT_AUTHENTICATION_SECRET=your-secret" >> .env
42
+ #
43
+ # Or export them directly in your environment:
44
+ # export EASY_CRYPT_AUTHENTICATION_CIPHER=aes128
45
+ # export EASY_CRYPT_AUTHENTICATION_SALT=your-salt
46
+ # export EASY_CRYPT_AUTHENTICATION_SECRET=your-secret
47
+ class EnvProvider < Base
48
+ # @return [String, nil] The encryption cipher from environment variable
49
+ def cipher
50
+ credentials('CIPHER')
51
+ end
52
+
53
+ # @return [String, nil] The salt value from environment variable
54
+ def salt
55
+ credentials('SALT')
56
+ end
57
+
58
+ # @return [String, nil] The secret key from environment variable
59
+ def secret
60
+ credentials('SECRET')
61
+ end
62
+
63
+ private
64
+
65
+ def credentials(key)
66
+ ENV.fetch("EASY_CRYPT_#{purpose.to_s.upcase}_#{key}", nil)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyCrypt
4
+ class SecretsProvider
5
+ # Provider that fetches encryption configuration from Rails credentials
6
+ #
7
+ # Configuration should be structured in credentials.yml.enc as:
8
+ #
9
+ # easy_crypt: # EasyCrypt purposes configuration parent key
10
+ # authentication: # Credential for the "authentication" purpose
11
+ # cipher: "aes128" # (optional) Cipher to use for the encryption
12
+ # # falls back to EasyCrypt.default_cipher is not set
13
+ # salt: "your-salt-value" # Salt value for the encryption
14
+ # secret: "your-secret-key" # Secret key for the encryption
15
+ # user_data: # Credential for the "user data" purpose
16
+ # salt: "different-salt" # Salt value for the encryption
17
+ # secret: "different-secret" # Secret key for the encryption
18
+ #
19
+ # You can define multiple credential purposes under the `easy_crypt` namespace,
20
+ # each with its own configuration. The credential purpose is specified when
21
+ # initializing the provider.
22
+ #
23
+ # To edit credentials.yml.enc, you can use one of these commands:
24
+ #
25
+ # With Visual Studio Code:
26
+ # EDITOR="code --wait" bin/rails credentials:edit
27
+ #
28
+ # With Vim:
29
+ # EDITOR="vim" bin/rails credentials:edit
30
+ #
31
+ # With Sublime Text:
32
+ # EDITOR="subl --wait" bin/rails credentials:edit
33
+ #
34
+ # With Nano:
35
+ # EDITOR="nano" bin/rails credentials:edit
36
+ #
37
+ # @example Configuration in credentials.yml.enc
38
+ # easy_crypt:
39
+ # authentication:
40
+ # cipher: aes128
41
+ # salt: "salt for the authentication purpose"
42
+ # secret: "secret for the authentication purpose"
43
+ # user_data:
44
+ # salt: "different-salt"
45
+ # secret: "different-secret"
46
+ #
47
+ # @example Accessing credentials
48
+ # # Using authentication credentials
49
+ # provider = RailsCredentialsProvider.new(:authentication)
50
+ # provider.cipher #=> "aes128"
51
+ # provider.salt #=> "salt for the authentication purpose"
52
+ # provider.secret #=> "secret for the authentication purpose"
53
+ #
54
+ # # Using user_data credentials
55
+ # provider = RailsCredentialsProvider.new(:user_data)
56
+ # provider.cipher #=> nil
57
+ # provider.salt #=> "different-salt"
58
+ # provider.secret #=> "different-secret"
59
+ class RailsCredentialsProvider < Base
60
+ # @return [String, nil] The encryption cipher from Rails credentials
61
+ def cipher
62
+ credentials&.cipher
63
+ end
64
+
65
+ # @return [String] The salt value from Rails credentials
66
+ def salt
67
+ credentials&.salt
68
+ end
69
+
70
+ # @return [String] The secret key from Rails credentials
71
+ def secret
72
+ credentials&.secret
73
+ end
74
+
75
+ private
76
+
77
+ def credentials
78
+ Rails.application.credentials.easy_crypt[purpose]
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'secrets_provider/base'
4
+ require_relative 'secrets_provider/env_provider'
5
+ require_relative 'secrets_provider/rails_credentials_provider'
6
+
7
+ module EasyCrypt
8
+ # The SecretsProvider class serves as a factory for creating secret providers
9
+ # that supply encryption configuration values from different sources.
10
+ #
11
+ # @example Creating a provider for Rails credentials and for the authentication purpose
12
+ # provider = SecretsProvider.build(:rails_credentials, :authentication)
13
+ #
14
+ # @example Creating a provider for environment variables and for the user_data purpose
15
+ # provider = SecretsProvider.build(:env_vars, :user_data)
16
+ class SecretsProvider
17
+ # Available secrets providers and their corresponding classes
18
+ PROVIDERS = {
19
+ env_vars: EnvProvider,
20
+ rails_credentials: RailsCredentialsProvider
21
+ }.freeze
22
+
23
+ # Builds a new secrets provider instance based on the specified provider
24
+ #
25
+ # This method acts as a factory for instantiating a secrets provider that fetches
26
+ # encryption configurations (e.g., secret keys, salts) for a given purpose from
27
+ # the selected source. The method ensures the specified provider exists and validates
28
+ # the configuration values.
29
+ #
30
+ # @param provider [Symbol] The secrets provider to use (:env_vars or :rails_credentials).
31
+ # - `:env_vars`: To fetch secrets from environment variables.
32
+ # - `:rails_credentials`: To fetch secrets from Rails credentials.
33
+ # @param purpose [Symbol] The purpose of the credential (e.g., :authentication, :user_data).
34
+ # This links the encryption secrets to a set of credentials (such as secret key and salt).
35
+ #
36
+ # @example Building a provider for Rails credentials with the `authentication` purpose
37
+ # provider = SecretsProvider.build(:rails_credentials, :authentication)
38
+ #
39
+ # @example Building a provider for environment variables with the `user_data` purpose
40
+ # provider = SecretsProvider.build(:env_vars, :user_data)
41
+ #
42
+ # @raise [InvalidSecretsProvider] If the specified secrets provider is not recognized.
43
+ #
44
+ # @return [Base] An instance of the appropriate provider class.
45
+ def self.build(provider, purpose)
46
+ provider = PROVIDERS
47
+ .fetch(provider.to_sym) { raise InvalidSecretsProvider, provider }
48
+ .new(purpose)
49
+ provider.validate_attributes!
50
+ provider
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyCrypt
4
+ VERSION = '1.0.0'
5
+ end
data/lib/easy_crypt.rb ADDED
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require 'active_support/message_encryptor'
5
+ require 'active_support/key_generator'
6
+
7
+ require_relative 'easy_crypt/exception'
8
+ require_relative 'easy_crypt/secrets_provider'
9
+
10
+ # EasyCrypt
11
+ # =========
12
+ #
13
+ # EasyCrypt is a Ruby utility that provides secure and flexible encryption/decryption capabilities
14
+ # for Ruby on Rails applications.
15
+ #
16
+ # Features
17
+ # --------
18
+ # * Multiple secrets providers support (environment variables and Rails credentials)
19
+ # * Configurable encryption cipher
20
+ # * Purpose-based encryption/decryption
21
+ # * Built on top of Rails' ActiveSupport::MessageEncryptor
22
+ # * Simple and intuitive API
23
+ #
24
+ # Installation
25
+ # ------------
26
+ # Add this line to your application's Gemfile:
27
+ # gem 'easy_crypt'
28
+ #
29
+ # Then execute:
30
+ # $ bundle install
31
+ #
32
+ # Or install it yourself as:
33
+ # $ gem install easy_crypt
34
+ #
35
+ # Configuration
36
+ # -------------
37
+ #
38
+ # Create an initializer at config/initializers/easy_crypt.rb:
39
+ #
40
+ # ```ruby
41
+ # EasyCrypt.configure do |config|
42
+ # # Choose your secrets provider. Currently supports :rails_credentials or :env_vars.
43
+ # # Default to :rails_credentials
44
+ # config.secrets_provider = :rails_credentials
45
+ #
46
+ # # Set your default cipher (optional)
47
+ # # Default to `ActiveSupport::MessageEncryptor.default_cipher` which is set to
48
+ # # 'aes-256-gcm' or 'aes-256-cbc' as of now, depending on your application configuration.
49
+ # # See `use_authenticated_message_encryption` configuration option for more information.
50
+ # # See `OpenSSL::Cipher.ciphers` for a list of accepted values.
51
+ # config.default_cipher = 'aes-256-gcm'
52
+ # end
53
+ #
54
+ # Usage
55
+ # ---
56
+ #
57
+ # First, you need to configure a secret and a salt for a specific purpose.
58
+ # To do so, you'll need to use one of the supported secrets providers (currently
59
+ # Rails credentials and env vars) and define a purpose and its credentials.
60
+ #
61
+ # See `EasyCrypt::SecretsProvider` subclasses for more information on how
62
+ # to configure the secrets provider of your choice.
63
+ #
64
+ # Given that you provided a secret and a salt for the "user_data" purpose,
65
+ # you are able to call the `encrypt_user_data` and `decrypt_user_data` methods
66
+ # to directly use the credentials of the "user_data" purpose.
67
+ #
68
+ # ```ruby
69
+ # # Encrypt a value
70
+ # encrypted = EasyCrypt.encrypt_user_data("sensitive information")
71
+ #
72
+ # # Decrypt a value
73
+ # decrypted = EasyCrypt.decrypt_user_data(encrypted)
74
+ # ```
75
+ #
76
+ module EasyCrypt
77
+ # @return [String] The default cipher used for encryption/decryption operations
78
+ @default_cipher = ActiveSupport::MessageEncryptor.default_cipher
79
+
80
+ # @return [Symbol] The provider used for retrieving encryption secrets
81
+ @secrets_provider = :rails_credentials
82
+
83
+ class << self
84
+ attr_accessor :default_cipher, :secrets_provider
85
+
86
+ # Configures the EasyCrypt module with custom settings
87
+ #
88
+ # @example
89
+ # EasyCrypt.configure do |config|
90
+ # config.secrets_provider = :env_vars
91
+ # config.default_cipher = 'aes-128-gcm'
92
+ # end
93
+ #
94
+ # @yieldparam config [EasyCrypt] The configuration object
95
+ # @return [void]
96
+ def configure
97
+ yield self
98
+ end
99
+
100
+ # Handles dynamic encryption/decryption method calls
101
+ #
102
+ # This method allows dynamically calling encryption or decryption methods for specific purposes
103
+ # by embedding the purpose directly in the method name. The pattern for such method names is:
104
+ # `encrypt_<purpose>` for encryption and `decrypt_<purpose>` for decryption.
105
+ #
106
+ # For instance:
107
+ # - Calling `EasyCrypt.encrypt_authentication(value)` will encrypt the provided value using the
108
+ # credentials configured for the `authentication` purpose.
109
+ # - Calling `EasyCrypt.decrypt_user_data(encrypted_value)` will decrypt the given value using the
110
+ # credentials for the `user_data` purpose.
111
+ #
112
+ # This dynamic approach enables simple and intuitive APIs without requiring explicit method
113
+ # definitions for each purpose.
114
+ #
115
+ # @example Encrypting a value for the "authentication" purpose
116
+ # encrypted = EasyCrypt.encrypt_authentication("my_secret_data")
117
+ #
118
+ # @example Decrypting a value for the "user_data" purpose
119
+ # decrypted = EasyCrypt.decrypt_user_data(encrypted_data)
120
+ #
121
+ # @note If the method name does not match the expected pattern, `method_missing` will defer to
122
+ # the superclass implementation, raising a `NoMethodError` if the method is undefined.
123
+ #
124
+ # @return [Object, String, nil] The encrypted or decrypted value, or nil if decryption fails
125
+ def method_missing(method_name, *args)
126
+ if /^(?<action>encrypt|decrypt)_(?<purpose>[a-z_]+)$/ =~ method_name
127
+ send(action, *args.unshift(purpose.to_sym))
128
+ else
129
+ super
130
+ end
131
+ end
132
+
133
+ # Checks if a method is supported by the module
134
+ def respond_to_missing?(method_name, include_private)
135
+ /^(encrypt|decrypt)_[a-z_]+$/ =~ method_name || super
136
+ end
137
+
138
+ private
139
+
140
+ # Encrypts and signs a value for a specific purpose
141
+ #
142
+ # @param purpose [Symbol] The purpose for which the value is being encrypted, e.g., :user_data
143
+ # @param value [Object] The value to encrypt (must be serializable)
144
+ # @param options [Hash] Additional options for encryption
145
+ #
146
+ # @example Encrypting a value for the "user_data" purpose
147
+ # EasyCrypt.encrypt_user_data("sensitive information")
148
+ #
149
+ # @return [String] The encrypted and signed value
150
+ def encrypt(purpose, value, options = {})
151
+ options.merge!(purpose: purpose)
152
+ encryptor(purpose).encrypt_and_sign(value, **options)
153
+ end
154
+
155
+ # Decrypts a value that was encrypted for a specific purpose
156
+ #
157
+ # @param purpose [Symbol] The purpose for which the value was encrypted, e.g., :user_data
158
+ # @param encrypted_data [String] The encrypted data to decrypt
159
+ #
160
+ # @example Decrypting a value for the "user_data" purpose
161
+ # EasyCrypt.decrypt_user_data(encrypted_value)
162
+ #
163
+ # @return [Object, nil] The decrypted value, or nil if decryption fails
164
+ def decrypt(purpose, encrypted_data)
165
+ encryptor(purpose).decrypt_and_verify(encrypted_data, purpose: purpose)
166
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
167
+ nil
168
+ end
169
+
170
+ # Creates a new MessageEncryptor instance for a specific purpose
171
+ #
172
+ # @param purpose [Symbol] The purpose for which to create the encryptor, e.g., :user_data
173
+ #
174
+ # @example Creating an encryptor configured for the "user_data" purpose
175
+ # encryptor = EasyCrypt.send(:encryptor, :user_data)
176
+ #
177
+ # @return [ActiveSupport::MessageEncryptor] The configured encryptor instance
178
+ def encryptor(purpose)
179
+ provider = SecretsProvider.build(secrets_provider, purpose)
180
+
181
+ cipher = provider.cipher || default_cipher
182
+
183
+ len = ActiveSupport::MessageEncryptor.key_len(cipher)
184
+ key = ActiveSupport::KeyGenerator.new(provider.secret).generate_key(provider.salt, len)
185
+ ActiveSupport::MessageEncryptor.new(key, cipher: cipher)
186
+ end
187
+ end
188
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: easy_crypt
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin Bouchet
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-01-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |2-
14
+
15
+ Encryption and decryption for Rails applications, based on ActiveSupport::MessageEncryptor, using
16
+ multiple secrets providers, and defining a simple, purpose-based encryption/decryption API.
17
+ email: randoum@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - CHANGELOG.md
23
+ - CODE_OF_CONDUCT.md
24
+ - LICENSE.txt
25
+ - README.md
26
+ - lib/easy_crypt.rb
27
+ - lib/easy_crypt/exception.rb
28
+ - lib/easy_crypt/secrets_provider.rb
29
+ - lib/easy_crypt/secrets_provider/base.rb
30
+ - lib/easy_crypt/secrets_provider/env_provider.rb
31
+ - lib/easy_crypt/secrets_provider/rails_credentials_provider.rb
32
+ - lib/easy_crypt/version.rb
33
+ homepage: https://github.com/randoum/easy_crypt
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://github.com/randoum/easy_crypt
38
+ documentation_uri: https://rubydoc.info/github/randoum/easy_crypt
39
+ changelog_uri: https://github.com/randoum/easy_crypt/blob/main/CHANGELOG.md
40
+ source_code_uri: https://github.com/randoum/easy_crypt
41
+ bug_tracker_uri: https://github.com/randoum/easy_crypt/issues
42
+ rubygems_mfa_required: 'true'
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 2.7.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 1.8.11
57
+ requirements: []
58
+ rubygems_version: 3.1.2
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Secure and flexible encryption for Ruby on Rails applications.
62
+ test_files: []