easy_crypt 1.0.0

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 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: []