secret_string 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: '059dcb7dea1885b6f2b24c01c70567028e24cf9d4a916e607ce98d3a8ec5baab'
4
+ data.tar.gz: 4fc32cecd334d8cddbe7f577b5997a09c437b8d89c2d61f243db0aac137ebdb8
5
+ SHA512:
6
+ metadata.gz: ae6503a05ddb781a1f79235630228e86c0b3c42984c43f4cbae9b331c1e7b3d456b604a523b3d4123de44b7434aea6aa4ec1c70e24410ac941d6481f7100de0c
7
+ data.tar.gz: 7725fe9c2bc7c17276b380ad75d68587958d1fcc6efaa1c8ed1d2591c971b91e8832966d745380933b309524ef8981409af04dd73ef59564aa8d1b0f9f9058fb
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # [v0.0.1](https://github.com/Muriel-Salvan/secret_string/compare/...v0.0.1) (2021-06-28 16:06:53)
2
+
3
+ ### Patches
4
+
5
+ * [Set Ruby 2.7](https://github.com/Muriel-Salvan/secret_string/commit/8c641d864d8c951fd6a40f36a614a070b66630b8)
data/LICENSE.md ADDED
@@ -0,0 +1,31 @@
1
+
2
+ The license stated herein is a copy of the BSD License (modified on July 1999).
3
+ The AUTHOR mentionned below refers to the list of people involved in the
4
+ creation and modification of any file included in the delivered package.
5
+ This list is found in the file named AUTHORS.
6
+ The AUTHORS and LICENSE files have to be included in any release of software
7
+ embedding source code of this package, or using it as a derivative software.
8
+
9
+ Copyright (c) 2021 Muriel Salvan (muriel@x-aeon.com)
10
+
11
+ Redistribution and use in source and binary forms, with or without
12
+ modification, are permitted provided that the following conditions are met:
13
+
14
+ 1. Redistributions of source code must retain the above copyright notice,
15
+ this list of conditions and the following disclaimer.
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+ 3. The name of the author may not be used to endorse or promote products
20
+ derived from this software without specific prior written permission.
21
+
22
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
23
+ WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
24
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
25
+ EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
26
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
27
+ OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
30
+ IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
31
+ OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
2
+
3
+ # secret_string - Remove secrets (passwords, keys...) from memory
4
+
5
+ ## Description
6
+
7
+ This Rubygem gives you ways to clean sensitive data (secrets, SSH keys, passwords) from your Ruby String variables in memory.
8
+
9
+ ## Install
10
+
11
+ Via gem
12
+
13
+ ``` bash
14
+ $ gem install secret_string
15
+ ```
16
+
17
+ If using `bundler`, add this in your `Gemfile`:
18
+
19
+ ``` ruby
20
+ gem 'secret_string'
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### With a protecting scope
26
+
27
+ The simplest and more robust way to use it is with the `SecretString.protect` method that gives a scope for a secret to be used from a String and will make sure this secret is removed from memory when code ends.
28
+
29
+ Example:
30
+ ```ruby
31
+ require 'secret_string'
32
+
33
+ secret = 'P4$Sw0rD' # Usually retrieved from a file, ENV var, user input...
34
+
35
+ # Create the protected scope
36
+ SecretString.protect(secret) do |secret_string|
37
+
38
+ # Let's try to display the password
39
+ puts "Password on screen is #{secret_string}"
40
+ # => Password on screen is XXXXX
41
+
42
+ # Its value can still be accessed using to_unprotected
43
+ puts "If we REALLY want to display it or use its real secret value somewhere: #{secret_string.to_unprotected}"
44
+ # => If we REALLY want to display it or use its real secret value somewhere: P4$Sw0rD
45
+
46
+ end
47
+
48
+ # Now that we are out of the protected scope, let's try leetch its value again!
49
+ puts "Outside of protection, my secret from memory is now #{secret}"
50
+ # => Outside of protection, my secret from memory is now
51
+ ```
52
+
53
+ You can also control the silenced secret, in case you still need to display it in logs or messages:
54
+ ```ruby
55
+ require 'secret_string'
56
+
57
+ SecretString.protect('P4$Sw0rD', silenced_str: '<FAKE PASSWORD>') do |secret_string|
58
+ # Let's try to display the password
59
+ puts "Password on screen is #{secret_string}"
60
+ # => Password on screen is <FAKE PASSWORD>
61
+ end
62
+ ```
63
+
64
+ ### With the `SecretString` class
65
+
66
+ If you need more control (like having a simple scope is not enough), you can directly use the `SecureString` class to protect your strings.
67
+ If you do so, don't forget to call the `erase` method to clean their data, and make sure you didn't clone the secret in other variables.
68
+
69
+ Example:
70
+ ```ruby
71
+ require 'secret_string'
72
+
73
+ my_secret = SecretString.new('P4$Sw0rD', silenced_str: '<FAKE PASSWORD>')
74
+
75
+ puts "My secret handled without precaution is #{my_secret}"
76
+ # => My secret handled without precaution is <FAKE PASSWORD>
77
+
78
+ puts "My secret when I REALLY want its value is #{my_secret.to_unprotected}"
79
+ # => My secret when I REALLY want its value is P4$Sw0rD
80
+
81
+ my_secret.erase
82
+
83
+ puts "My secret after being erased is #{my_secret.to_unprotected}"
84
+ # => My secret after being erased is
85
+ ```
86
+
87
+ ### Dealing directly with original Strings
88
+
89
+ You can erase any Ruby String using `SecretString.erase` method:
90
+
91
+ ```ruby
92
+ require 'secret_string'
93
+
94
+ my_secret = 'P4$Sw0rD'
95
+
96
+ puts "My secret before erase is #{my_secret}"
97
+ # => My secret before erase is P4$Sw0rD
98
+
99
+ SecretString.erase(my_secret)
100
+
101
+ puts "My secret after being erased is #{my_secret}"
102
+ # => My secret after being erased is
103
+ ```
104
+
105
+ The `erase` and `to_unprotected` methods have also been added to the core String class so that you can treat both `String` and `SecretString` easily within your code logic (no need to plague your code with `if str.is_a?(SecureString)`). Those methods won't do or change anything to the normal Ruby Strings.
106
+
107
+ Example:
108
+ ```ruby
109
+ require 'secret_string'
110
+
111
+ strings = [
112
+ 'Normal string',
113
+ SecretString.new('Secret: P4$Sw0rD', silenced_str: 'Secret: NO WAY')
114
+ ]
115
+
116
+ puts "My strings are: #{strings}"
117
+ # => My strings are: ["Normal string", "Secret: NO WAY"]
118
+
119
+ puts "My unprotected strings are: #{strings.map(&:to_unprotected)}"
120
+ # => My unprotected strings are: ["Normal string", "Secret: P4$Sw0rD"
121
+ ```
122
+
123
+ ## Change log
124
+
125
+ Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
126
+
127
+ ## Testing
128
+
129
+ Automated tests are done using rspec.
130
+
131
+ To execute them, first install development dependencies:
132
+
133
+ ```bash
134
+ bundle install
135
+ ```
136
+
137
+ Then execute rspec
138
+
139
+ ```bash
140
+ bundle exec rspec
141
+ ```
142
+
143
+ Manual testing has been done using `gdb` to effectively check that a Ruby process dumping its full memory on disk does not reveal secrets once erased by `SecretString` (tested on Ruby 2.7).
144
+
145
+ ## Contributing
146
+
147
+ Any contribution is welcome:
148
+ * Fork the github project and create pull requests.
149
+ * Report bugs by creating tickets.
150
+ * Suggest improvements and new features by creating tickets.
151
+
152
+ ## Credits
153
+
154
+ - [Muriel Salvan](https://x-aeon.com/muriel)
155
+
156
+ ## License
157
+
158
+ The BSD License. Please see [License File](LICENSE.md) for more information.
@@ -0,0 +1,69 @@
1
+ require 'forwardable'
2
+ require 'stringio'
3
+ require 'secret_string/core_extensions/string'
4
+
5
+ # Protect sensitive data in Strings by erasing it from memory when not needed anymore.
6
+ class SecretString
7
+
8
+ class << self
9
+
10
+ # Securely erase a String from memory
11
+ #
12
+ # Parameters::
13
+ # * *secret* (String): The secret to erase from memory
14
+ def erase(secret)
15
+ secret_size = secret.bytesize
16
+ io = StringIO.new("\0" * secret_size)
17
+ io.read(secret_size, secret)
18
+ end
19
+
20
+ # Protect a String by giving access only to a secured version of it.
21
+ # Make sure the String will be erased at the end of its access.
22
+ #
23
+ # Parameters::
24
+ # * *str* (String): String to protect
25
+ # * *silenced_str* (String): The protected representation of this string [default: 'XXXXX']
26
+ # * Proc: Code called with the string secured
27
+ # * Parameters::
28
+ # * *secretstring* (SecretString): The secret string
29
+ def protect(str, silenced_str: 'XXXXX')
30
+ secret_string = SecretString.new(str, silenced_str: silenced_str)
31
+ yield secret_string
32
+ ensure
33
+ secret_string.erase
34
+ end
35
+
36
+ end
37
+
38
+ # Constructor
39
+ #
40
+ # Parameters::
41
+ # * *str* (String): The original string to protect
42
+ # * *silenced_str* (String): The silenced representation of this string [default: 'XXXXX']
43
+ def initialize(str, silenced_str: 'XXXXX')
44
+ @str = str
45
+ # Make sure we manipulate @str without cloning or modifying it from now on.
46
+ @silenced_str = silenced_str
47
+ end
48
+
49
+ # Delegate the String representations methods to the silenced String by default
50
+ extend Forwardable
51
+ def_delegators :@silenced_str, *%i[
52
+ inspect
53
+ to_s
54
+ ]
55
+
56
+ # Return the unprotected String
57
+ #
58
+ # Result::
59
+ # * String: Unprotected string
60
+ def to_unprotected
61
+ @str
62
+ end
63
+
64
+ # Erase the string
65
+ def erase
66
+ SecretString.erase(@str)
67
+ end
68
+
69
+ end
@@ -0,0 +1,28 @@
1
+ class SecretString
2
+
3
+ module CoreExtensions
4
+
5
+ # Make sure a String can be used as a SecretString.
6
+ # This helps in avoiding code like "if my_str.is_a?(SecretString)" to access to its content.
7
+ module String
8
+
9
+ # Return the unprotected String
10
+ #
11
+ # Result::
12
+ # * String: Unprotected string
13
+ def to_unprotected
14
+ self
15
+ end
16
+
17
+ # Erase the string
18
+ def erase
19
+ # Nothing to do
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+
28
+ String.include SecretString::CoreExtensions::String
@@ -0,0 +1,5 @@
1
+ class SecretString
2
+
3
+ VERSION = '1.0.0'
4
+
5
+ end
@@ -0,0 +1 @@
1
+ require 'secret_string'
@@ -0,0 +1,31 @@
1
+ require 'json'
2
+
3
+ describe 'Coding guidelines' do
4
+
5
+ it 'makes sure code style follow Rubocop guides' do
6
+ rubocop_report = JSON.parse(`bundle exec rubocop --format json`)
7
+ expect(rubocop_report['summary']['offense_count']).to(
8
+ eq(0),
9
+ proc do
10
+ # Format a great error message to help
11
+ wrong_files = rubocop_report['files'].reject { |file_info| file_info['offenses'].empty? }
12
+ <<~EO_ERROR
13
+ #{wrong_files.size} files have Rubocop issues:
14
+ #{
15
+ wrong_files.map do |file_info|
16
+ offenses = file_info['offenses'].map { |offense_info| "L#{offense_info['location']['start_line']}: #{offense_info['cop_name']} - #{offense_info['message']}" }
17
+ "* #{file_info['path']}:#{
18
+ if offenses.size == 1
19
+ " #{offenses.first}"
20
+ else
21
+ " #{offenses.size} offenses:\n#{offenses.map { |offense| " - #{offense}" }.join("\n")}"
22
+ end
23
+ }"
24
+ end.join("\n")
25
+ }
26
+ EO_ERROR
27
+ end
28
+ )
29
+ end
30
+
31
+ end
@@ -0,0 +1,50 @@
1
+ describe SecretString do
2
+
3
+ context 'with a silenced string' do
4
+
5
+ subject(:secret) { described_class.new('MySecret', silenced_str: 'SilencedString') }
6
+
7
+ it 'silences using to_s' do
8
+ expect(secret.to_s).to eq 'SilencedString'
9
+ end
10
+
11
+ it 'silences using inspect' do
12
+ expect(secret.inspect).to eq '"SilencedString"'
13
+ end
14
+
15
+ it 'reveals using to_unprotected' do
16
+ expect(secret.to_unprotected).to eq 'MySecret'
17
+ end
18
+
19
+ it 'erases the data in memory' do
20
+ secret.erase
21
+ expect(secret.to_unprotected).not_to eq 'MySecret'
22
+ end
23
+
24
+ end
25
+
26
+ describe 'erase' do
27
+
28
+ it 'erases a String' do
29
+ str = 'MySecret'
30
+ described_class.erase(str)
31
+ expect(str).not_to eq 'MySecret'
32
+ end
33
+
34
+ end
35
+
36
+ describe 'protect' do
37
+
38
+ it 'protects a String' do
39
+ str = 'MySecret'
40
+ described_class.protect(str, silenced_str: 'SilencedString') do |secret_string|
41
+ expect(secret_string.to_s).to eq 'SilencedString'
42
+ expect(secret_string.to_unprotected).to eq 'MySecret'
43
+ expect(str.to_s).to eq 'MySecret'
44
+ end
45
+ expect(str.to_s).not_to eq 'MySecret'
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,13 @@
1
+ describe String do
2
+
3
+ it 'implements the same API as SecretString: to_unprotected' do
4
+ expect('MySecret'.to_unprotected).to eq 'MySecret'
5
+ end
6
+
7
+ it 'implements the same API as SecretString: erase' do
8
+ str = 'MySecret'
9
+ expect { str.erase }.not_to raise_error
10
+ expect(str).to eq 'MySecret'
11
+ end
12
+
13
+ end
@@ -0,0 +1,102 @@
1
+ require 'secret_string_test/helpers'
2
+
3
+ # This file was generated by the `rspec --init` command. Conventionally, all
4
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
6
+ # this file to always be loaded, without a need to explicitly require it in any
7
+ # files.
8
+ #
9
+ # Given that it is always loaded, you are encouraged to keep this file as
10
+ # light-weight as possible. Requiring heavyweight dependencies from this file
11
+ # will add to the boot time of your test suite on EVERY test run, even for an
12
+ # individual file that may not need all of that loaded. Instead, consider making
13
+ # a separate helper file that requires the additional dependencies and performs
14
+ # the additional setup, and require it from the spec files that actually need
15
+ # it.
16
+ #
17
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
18
+ RSpec.configure do |config|
19
+ # rspec-expectations config goes here. You can use an alternate
20
+ # assertion/expectation library such as wrong or the stdlib/minitest
21
+ # assertions if you prefer.
22
+ config.expect_with :rspec do |expectations|
23
+ # This option will default to `true` in RSpec 4. It makes the `description`
24
+ # and `failure_message` of custom matchers include text for helper methods
25
+ # defined using `chain`, e.g.:
26
+ # be_bigger_than(2).and_smaller_than(4).description
27
+ # # => "be bigger than 2 and smaller than 4"
28
+ # ...rather than:
29
+ # # => "be bigger than 2"
30
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
31
+ end
32
+
33
+ # rspec-mocks config goes here. You can use an alternate test double
34
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
35
+ config.mock_with :rspec do |mocks|
36
+ # Prevents you from mocking or stubbing a method that does not exist on
37
+ # a real object. This is generally recommended, and will default to
38
+ # `true` in RSpec 4.
39
+ mocks.verify_partial_doubles = true
40
+ end
41
+
42
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
43
+ # have no way to turn it off -- the option exists only for backwards
44
+ # compatibility in RSpec 3). It causes shared context metadata to be
45
+ # inherited by the metadata hash of host groups and examples, rather than
46
+ # triggering implicit auto-inclusion in groups with matching metadata.
47
+ config.shared_context_metadata_behavior = :apply_to_host_groups
48
+
49
+ # The settings below are suggested to provide a good initial experience
50
+ # with RSpec, but feel free to customize to your heart's content.
51
+
52
+ # This allows you to limit a spec run to individual examples or groups
53
+ # you care about by tagging them with `:focus` metadata. When nothing
54
+ # is tagged with `:focus`, all examples get run. RSpec also provides
55
+ # aliases for `it`, `describe`, and `context` that include `:focus`
56
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
57
+ # config.filter_run_when_matching :focus
58
+
59
+ # Allows RSpec to persist some state between runs in order to support
60
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
61
+ # you configure your source control system to ignore this file.
62
+ # config.example_status_persistence_file_path = "spec/examples.txt"
63
+
64
+ # Limits the available syntax to the non-monkey patched syntax that is
65
+ # recommended. For more details, see:
66
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
67
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
68
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
69
+ # config.disable_monkey_patching!
70
+
71
+ # This setting enables warnings. It's recommended, but in some cases may
72
+ # be too noisy due to issues in dependencies.
73
+ # config.warnings = true
74
+
75
+ # Many RSpec users commonly either run the entire suite or an individual
76
+ # file, and it's useful to allow more verbose output when running an
77
+ # individual spec file.
78
+ # if config.files_to_run.one?
79
+ # # Use the documentation formatter for detailed output,
80
+ # # unless a formatter has already been configured
81
+ # # (e.g. via a command-line flag).
82
+ # config.default_formatter = "doc"
83
+ # end
84
+
85
+ # Print the 10 slowest examples and example groups at the
86
+ # end of the spec run, to help surface which specs are running
87
+ # particularly slow.
88
+ # config.profile_examples = 10
89
+
90
+ # Run specs in random order to surface order dependencies. If you find an
91
+ # order dependency and want to debug it, you can fix the order by providing
92
+ # the seed, which is printed after each run.
93
+ # --seed 1234
94
+ # config.order = :random
95
+
96
+ # Seed global randomization in this process using the `--seed` CLI option.
97
+ # Setting this allows you to use `--seed` to deterministically reproduce
98
+ # test failures related to randomization by passing the same `--seed` value
99
+ # as the one that triggered the failure.
100
+ # Kernel.srand config.seed
101
+
102
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: secret_string
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Muriel Salvan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-06-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.8'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sem_ver_components
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.16'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.16'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.4'
69
+ description: Remove secrets (passwords, keys...) from memory
70
+ email:
71
+ - muriel@x-aeon.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files:
75
+ - CHANGELOG.md
76
+ - LICENSE.md
77
+ - README.md
78
+ files:
79
+ - CHANGELOG.md
80
+ - LICENSE.md
81
+ - README.md
82
+ - lib/secret_string.rb
83
+ - lib/secret_string/core_extensions/string.rb
84
+ - lib/secret_string/version.rb
85
+ - spec/secret_string_test/helpers.rb
86
+ - spec/secret_string_test/tests/rubocop_spec.rb
87
+ - spec/secret_string_test/tests/secret_string_spec.rb
88
+ - spec/secret_string_test/tests/string_spec.rb
89
+ - spec/spec_helper.rb
90
+ homepage:
91
+ licenses:
92
+ - BSD-3-Clause
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.7'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.1.6
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Secret String
113
+ test_files: []