sym 2.8.1 → 3.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.
Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +31 -30
  3. data/.envrc +7 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +150 -928
  6. data/.travis.yml +16 -26
  7. data/CHANGELOG.md +220 -167
  8. data/Gemfile +1 -0
  9. data/LICENSE +2 -2
  10. data/README.adoc +670 -0
  11. data/Rakefile +10 -4
  12. data/bin/changelog +34 -0
  13. data/bin/sym.completion.bash +6 -4
  14. data/bin/sym.symit.bash +412 -187
  15. data/codecov.yml +29 -0
  16. data/design/sym-class-dependency-future-refactor.png +0 -0
  17. data/design/sym-class-dependency-vertical.png +0 -0
  18. data/design/sym-class-dependency.graffle +0 -0
  19. data/design/sym-class-dependency.png +0 -0
  20. data/design/sym-help.png +0 -0
  21. data/exe/keychain +1 -1
  22. data/exe/sym +5 -2
  23. data/lib/ruby_warnings.rb +7 -0
  24. data/lib/sym.rb +2 -8
  25. data/lib/sym/app.rb +1 -2
  26. data/lib/sym/app/args.rb +3 -2
  27. data/lib/sym/app/cli.rb +34 -21
  28. data/lib/sym/app/cli_slop.rb +9 -2
  29. data/lib/sym/app/commands.rb +1 -1
  30. data/lib/sym/app/commands/base_command.rb +1 -1
  31. data/lib/sym/app/commands/bash_completion.rb +2 -2
  32. data/lib/sym/app/commands/open_editor.rb +1 -1
  33. data/lib/sym/app/commands/password_protect_key.rb +4 -4
  34. data/lib/sym/app/commands/show_examples.rb +1 -1
  35. data/lib/sym/app/input/handler.rb +7 -1
  36. data/lib/sym/app/keychain.rb +15 -9
  37. data/lib/sym/app/output/noop.rb +2 -1
  38. data/lib/sym/app/password/cache.rb +1 -1
  39. data/lib/sym/app/password/providers.rb +2 -3
  40. data/lib/sym/app/private_key/decryptor.rb +2 -2
  41. data/lib/sym/app/private_key/detector.rb +4 -7
  42. data/lib/sym/application.rb +6 -11
  43. data/lib/sym/constants.rb +39 -23
  44. data/lib/sym/data/wrapper_struct.rb +20 -12
  45. data/lib/sym/errors.rb +13 -2
  46. data/lib/sym/extensions/instance_methods.rb +7 -8
  47. data/lib/sym/extensions/stdlib.rb +0 -1
  48. data/lib/sym/extensions/with_retry.rb +1 -1
  49. data/lib/sym/extensions/with_timeout.rb +1 -1
  50. data/lib/sym/version.rb +54 -5
  51. data/sym.gemspec +36 -35
  52. metadata +102 -66
  53. data/.codeclimate.yml +0 -30
  54. data/README.md +0 -623
  55. data/lib/sym/app/password/providers/drb_provider.rb +0 -41
@@ -15,7 +15,7 @@ module Sym
15
15
  self.providers << provider_class
16
16
  end
17
17
 
18
- # Detect first instance that is "alive?" and return it.
18
+ # Detect first instance tht is "alive?" and return it.
19
19
  def detect
20
20
  self.detected ||= self.providers.inject(nil) do |instance, provider_class|
21
21
  instance || (p = provider_class.new; p.alive? ? p : nil)
@@ -38,7 +38,7 @@ module Sym
38
38
 
39
39
  def provider_from_argument(p, **opts, &block)
40
40
  case p
41
- when String, Symbol
41
+ when String, Symbol
42
42
  provider_class_name = "#{p.to_s.capitalize}Provider"
43
43
  Sym::App::Password::Providers.const_defined?(provider_class_name) ?
44
44
  Sym::App::Password::Providers.const_get(provider_class_name).new(**opts, &block) :
@@ -53,4 +53,3 @@ end
53
53
 
54
54
  # Order is important — they are tried in this order for auto detect
55
55
  require 'sym/app/password/providers/memcached_provider'
56
- require 'sym/app/password/providers/drb_provider'
@@ -31,10 +31,10 @@ module Sym
31
31
  rescue ::OpenSSL::Cipher::CipherError => e
32
32
  input_handler.puts 'Invalid password. Please try again.'
33
33
 
34
- if ((retries += 1) < 3)
34
+ if (retries += 1) < 3
35
35
  retry
36
36
  else
37
- raise(Sym::Errors::InvalidPasswordProvidedForThePrivateKey.new('Invalid password.'))
37
+ raise(Sym::Errors::WrongPasswordForKey.new('Invalid password.'))
38
38
  end
39
39
  end
40
40
  else
@@ -23,11 +23,10 @@ module Sym
23
23
  # procs on a given string.
24
24
  def read!
25
25
  KeySourceCheck::CHECKS.each do |source_check|
26
- if result = source_check.detect(self) rescue nil
27
- if key_ = normalize_key(result.key)
28
- key_source_ = result.to_s
29
- return key_, key_source_
30
- end
26
+ next unless result = source_check.detect(self) rescue nil
27
+ if key_ = normalize_key(result.key)
28
+ key_source_ = result.to_s
29
+ return key_, key_source_
31
30
  end
32
31
  end
33
32
  nil
@@ -51,8 +50,6 @@ module Sym
51
50
  rescue
52
51
  nil
53
52
  end
54
- else
55
- nil
56
53
  end
57
54
  end
58
55
  end
@@ -32,7 +32,6 @@ module Sym
32
32
  :stdin, :stdout, :stderr, :kernel
33
33
 
34
34
  def initialize(opts, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = nil)
35
-
36
35
  self.stdin = stdin
37
36
  self.stdout = stdout
38
37
  self.stderr = stderr
@@ -111,16 +110,12 @@ module Sym
111
110
  end
112
111
 
113
112
  def editor
114
- editors_to_try.find { |editor| File.exist?(editor) }
113
+ editors_to_try.compact.find { |editor| File.exist?(editor) }
115
114
  end
116
115
 
117
116
  def process_output(result)
118
- unless result.is_a?(Hash)
119
- self.output.call(result)
120
- result
121
- else
122
- result
123
- end
117
+ self.output.call(result) unless result.is_a?(Hash)
118
+ result
124
119
  end
125
120
 
126
121
  private
@@ -182,7 +177,7 @@ module Sym
182
177
  args[:verbose] = opts[:verbose]
183
178
  args[:provider] = opts[:cache_provider] if opts[:cache_provider]
184
179
 
185
- self.password_cache = Sym::App::Password::Cache.instance.configure(args)
180
+ self.password_cache = Sym::App::Password::Cache.instance.configure(**args)
186
181
  end
187
182
 
188
183
  def process_edit_option
@@ -207,7 +202,7 @@ module Sym
207
202
  end
208
203
 
209
204
  def initialize_action
210
- self.action = if opts[:encrypt] then
205
+ self.action = if opts[:encrypt]
211
206
  :encr
212
207
  elsif opts[:decrypt]
213
208
  :decr
@@ -217,7 +212,7 @@ module Sym
217
212
  # If we are encrypting or decrypting, and no data has been provided, check if we
218
213
  # should read from STDIN
219
214
  def initialize_data_source
220
- if self.action && opts[:string].nil? && opts[:file].nil? && !(self.stdin.tty?)
215
+ if self.action && opts[:string].nil? && opts[:file].nil? && !self.stdin.tty?
221
216
  opts[:file] = '-'
222
217
  end
223
218
  end
@@ -1,43 +1,59 @@
1
1
  require 'logger'
2
2
  module Sym
3
+ #
4
+ # This module is responsible for installing Sym BASH extensions.
5
+ #
3
6
  module Constants
4
- module Bash
5
7
 
6
- BASH_FILES = Dir.glob("#{File.expand_path('../../../bin', __FILE__)}/sym.*.bash").freeze
8
+ BASH_FILES = Dir.glob("#{File.expand_path('../../bin', __dir__)}/sym.*.bash").freeze
7
9
 
8
- Config = {}
10
+ class << self
11
+ attr_reader :user_home
9
12
 
10
- class << self
11
- def register_bash_files!
12
- BASH_FILES.each do |bash_file|
13
- register_bash_extension bash_file, Config
14
- end
15
- end
13
+ def user_home=(value)
14
+ @user_home = value
15
+ register_bash_files!
16
+ end
16
17
 
17
- private
18
+ def config
19
+ @config ||= {}
20
+ end
18
21
 
19
- def register_bash_extension(bash_file, hash)
20
- source_file = File.basename(bash_file)
21
- home_file = "#{Dir.home}/.#{source_file}"
22
+ def sym_key_file
23
+ "#{user_home}/.sym.key"
24
+ end
22
25
 
23
- hash[source_file.gsub(/sym\./, '').gsub(/\.bash/, '').to_sym] = {
24
- dest: home_file,
25
- source: bash_file,
26
- script: "[[ -f #{home_file} ]] && source #{home_file}"
27
- }
26
+ def register_bash_files!
27
+ BASH_FILES.each do |bash_file|
28
+ register_bash_extension bash_file
28
29
  end
29
30
  end
30
31
 
31
- self.register_bash_files!
32
+ private
33
+
34
+ def register_bash_extension(bash_file)
35
+ return unless user_home && Dir.exist?(user_home)
36
+
37
+ source_file = File.basename(bash_file)
38
+ home_file = "#{user_home}/.#{source_file}"
39
+ config_key = source_file.gsub(/sym\./, '').gsub(/\.bash/, '').to_sym
40
+
41
+ config[config_key] = {
42
+ dest: home_file,
43
+ source: bash_file,
44
+ script: "[[ -f #{home_file} ]] && source #{home_file}"
45
+ }
46
+ end
32
47
  end
33
48
 
49
+ self.user_home ||= ::Dir.home rescue nil
50
+ self.user_home ||= '/tmp'
51
+
52
+ self.register_bash_files!
53
+
34
54
  module Log
35
55
  NIL = Logger.new(nil).freeze # empty logger
36
56
  LOG = Logger.new(STDERR).freeze
37
57
  end
38
-
39
- ENV_ARGS_VARIABLE_NAME = 'SYM_ARGS'.freeze
40
- SYM_KEY_FILE = "#{ENV['HOME']}/.sym.key"
41
-
42
58
  end
43
59
  end
@@ -2,24 +2,32 @@ require 'sym/errors'
2
2
  module Sym
3
3
  module Data
4
4
  class WrapperStruct < Struct.new(
5
- :encrypted_data, # [Blob] Binary encrypted data (possibly compressed)
6
- :iv, # [String] IV used to encrypt the data
7
- :cipher_name, # [String] Name of the cipher used
8
- :salt, # [Integer] For password-encrypted data this is the salt
9
- :version, # [Integer] Version of the cipher used
10
- :compress # [Boolean] indicates if compression should be applied
11
- )
5
+ # [Blob] Binary encrypted data (possibly compressed)s
6
+ :encrypted_data,
7
+ # [String] IV used to encrypt the datas
8
+ :iv,
9
+ # [String] Name of the cipher used
10
+ :cipher_name,
11
+ # [Integer] For password-encrypted data this is the salt
12
+ :salt,
13
+ # [Integer] Version of the cipher used
14
+ :version,
15
+ # [Boolean] indicates if compression should be applied
16
+ :compress
17
+ )
18
+
19
+ define_singleton_method(:new, Class.method(:new))
12
20
 
13
21
  VERSION = 1
14
22
 
15
23
  attr_accessor :compressed
16
24
 
17
25
  def initialize(
18
- encrypted_data:, # [Blob] Binary encrypted data (possibly compressed)
19
- iv:, # [String] IV used to encrypt the data
20
- cipher_name:, # [String] Name of the cipher used
21
- salt: nil, # [Integer] For password-encrypted data this is the salt
22
- version: VERSION, # [Integer] Version of the cipher used
26
+ encrypted_data:,
27
+ iv:,
28
+ cipher_name:,
29
+ salt: nil,
30
+ version: VERSION,
23
31
  compress: Sym::Configuration.config.compression_enabled
24
32
  )
25
33
  super(encrypted_data, iv, cipher_name, salt, version, compress)
@@ -1,16 +1,23 @@
1
1
  module Sym
2
2
  # All public exceptions of this library are here.
3
3
  module Errors
4
+ # @formatter:off
4
5
  # Exceptions superclass for this library.
5
- class Sym::Errors::Error < StandardError; end
6
+ class Error < StandardError; end
6
7
 
7
8
  # No secret has been provided for encryption or decryption
8
9
  class InsufficientOptionsError < Sym::Errors::Error; end
9
10
 
10
11
  class PasswordError < Sym::Errors::Error; end
12
+
13
+ class InvalidSymHomeDirectory < Sym::Errors::Error; end
14
+
11
15
  class NoPasswordProvided < Sym::Errors::PasswordError; end
16
+
12
17
  class PasswordsDontMatch < Sym::Errors::PasswordError; end
18
+
13
19
  class PasswordTooShort < Sym::Errors::PasswordError; end
20
+
14
21
  class CantReadPasswordNoTTY < Sym::Errors::PasswordError; end
15
22
 
16
23
  class EditorExitedAbnormally < Sym::Errors::Error; end
@@ -20,13 +27,17 @@ module Sym
20
27
  class DataEncodingVersionMismatch< Sym::Errors::Error; end
21
28
 
22
29
  class KeyError < Sym::Errors::Error; end
30
+
23
31
  class InvalidEncodingPrivateKey < Sym::Errors::KeyError; end
24
- class InvalidPasswordProvidedForThePrivateKey < Sym::Errors::KeyError; end
32
+
33
+ class WrongPasswordForKey < Sym::Errors::KeyError; end
34
+
25
35
  class NoPrivateKeyFound < Sym::Errors::KeyError; end
26
36
 
27
37
  class NoDataProvided < Sym::Errors::Error; end
28
38
 
29
39
  class KeyChainCommandError < Sym::Errors::Error; end
40
+ # @formatter:on
30
41
 
31
42
  # Method was called on an abstract class. Override such methods in
32
43
  # subclasses, and use subclasses for instantiation of objects.
@@ -71,7 +71,7 @@ module Sym
71
71
  def make_password_key(cipher, password, salt = nil)
72
72
  key_len = cipher.key_len
73
73
  salt ||= OpenSSL::Random.random_bytes 16
74
- iter = 20000
74
+ iter = 20_000
75
75
  digest = OpenSSL::Digest::SHA256.new
76
76
  key = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iter, key_len, digest)
77
77
  return key, salt
@@ -87,12 +87,12 @@ module Sym
87
87
  block.call(cipher_struct) if block
88
88
 
89
89
  encrypted_data = update_cipher(cipher_struct.cipher, data)
90
- wrapper_struct = WrapperStruct.new(
91
- encrypted_data: encrypted_data,
92
- iv: cipher_struct.iv,
93
- cipher_name: cipher_struct.cipher.name,
94
- salt: cipher_struct.salt,
95
- compress: !compression_enabled)
90
+ arguments = { encrypted_data: encrypted_data,
91
+ iv: cipher_struct.iv,
92
+ cipher_name: cipher_struct.cipher.name,
93
+ salt: cipher_struct.salt,
94
+ compress: !compression_enabled }
95
+ wrapper_struct = WrapperStruct.new(arguments)
96
96
  encode(wrapper_struct, false)
97
97
  end
98
98
 
@@ -107,7 +107,6 @@ module Sym
107
107
  decode(update_cipher(cipher_struct.cipher, wrapper_struct.encrypted_data))
108
108
  end
109
109
 
110
-
111
110
  def encode_incoming_data(data)
112
111
  compression_enabled = !data.respond_to?(:size) || (data.size > 100 && encryption_config.compression_enabled)
113
112
  data = encode(data, compression_enabled)
@@ -1,4 +1,3 @@
1
-
2
1
  module Kernel
3
2
  def require_dir(___dir)
4
3
  @___dir ||= File.dirname(__FILE__)
@@ -2,7 +2,7 @@ module Sym
2
2
  module Extensions
3
3
  module WithRetry
4
4
 
5
- def with_retry(retries: 3, fail_block: nil, &block)
5
+ def with_retry(retries: 3, fail_block: nil)
6
6
  attempts = 0
7
7
  yield if block_given?
8
8
  rescue StandardError => e
@@ -3,7 +3,7 @@ module Sym
3
3
  module WithTimeout
4
4
 
5
5
  def with_timeout(timeout = 3)
6
- status = Timeout::timeout(timeout) {
6
+ status = Timeout.timeout(timeout) {
7
7
  yield if block_given?
8
8
  }
9
9
  end
@@ -1,8 +1,57 @@
1
1
  module Sym
2
- VERSION = '2.8.1'
3
- DESCRIPTION = <<-eof
4
- Sym is a ruby library (gem) that offers both the command line interface (CLI) and a set of rich Ruby APIs, which make it rather trivial to add encryption and decryption of sensitive data to your development or deployment flow. As a layer of additional security, you can encrypt the private key itself with a password. Unlike many other existing encryption tools, Sym focuses on getting out of the way — by offering its streamlined interface, hoping to make encryption of application secrets nearly completely transparent to the developers. For the data encryption Sym uses a symmetric 256-bit key with the AES-256-CBC cipher, same cipher as used by the US Government. For password-protecting the key Sym uses AES-128-CBC cipher. The resulting data is zlib-compressed and base64-encoded. The keys are also base64 encoded for easy copying/pasting/etc.
5
-
6
- Sym accomplishes encryption transparency by combining convenience features: 1) Sym can read the private key from multiple source types, such as: a pathname to a file, an environment variable name, a keychain entry, or CLI argument. You simply pass either of these to the -k flag — one flag that works for all source types. 2) By utilizing OS-X Keychain on a Mac, Sym offers truly secure way of storing the key on a local machine, much more secure then storing it on a file system, 3) By using a local password cache (activated with -c) via an in-memory provider such as memcached or drb, sym invocations take advantage of password cache, and only ask for a password once per a configurable time period, 4) By using SYM_ARGS environment variable, where common flags can be saved. This is activated with sym -A, 5) By reading the key from the default key source file ~/.sym.key which requires no flags at all, 6) By utilizing the --negate option to quickly encrypt a regular file, or decrypt an encrypted file with extension .enc 7) By implementing the -t (edit) mode, that opens an encrypted file in your $EDITOR, and replaces the encrypted version upon save & exit, optionally creating a backup. 8) By offering the Sym::MagicFile ruby API to easily read encrypted files into memory.
2
+ VERSION = '3.0.0'
3
+ DESCRIPTION = <<~eof
4
+
5
+ Sym is a ruby library (gem) that offers both the command line interface
6
+ (CLI) and a set of rich Ruby APIs, which make it rather trivial to add
7
+ encryption and decryption of sensitive data to your development or deployment
8
+ workflow.
9
+
10
+ For additional security the private key itself can be encrypted with a
11
+ user-generated password. For decryption using the key the password can be
12
+ input into STDIN, or be defined by an ENV variable, or an OS-X Keychain Entry.
13
+
14
+ Unlike many other existing encryption tools, Sym focuses on getting out of
15
+ your way by offering a streamlined interface with password caching (if
16
+ MemCached is installed and running locally) in hopes to make encryption of
17
+ application secrets nearly completely transparent to the developers.
18
+
19
+ Sym uses symmetric 256-bit key encryption with the AES-256-CBC cipher,
20
+ same cipher as used by the US Government.
21
+
22
+ For password-protecting the key Sym uses AES-128-CBC cipher. The resulting
23
+ data is zlib-compressed and base64-encoded. The keys are also base64 encoded
24
+ for easy copying/pasting/etc.
25
+
26
+ Sym accomplishes encryption transparency by combining several convenient features:
27
+
28
+ 1. Sym can read the private key from multiple source types, such as pathname,
29
+ an environment variable name, a keychain entry, or CLI argument. You simply
30
+ pass either of these to the -k flag — one flag that works for all source types.
31
+
32
+ 2. By utilizing OS-X Keychain on a Mac, Sym offers truly secure way of
33
+ storing the key on a local machine, much more secure then storing it on a file system,
34
+
35
+ 3. By using a local password cache (activated with -c) via an in-memory provider
36
+ such as memcached, sym invocations take advantage of password cache, and
37
+ only ask for a password once per a configurable time period,
38
+
39
+ 4. By using SYM_ARGS environment variable, where common flags can be saved. This
40
+ is activated with sym -A,
41
+
42
+ 5. By reading the key from the default key source file ~/.sym.key which
43
+ requires no flags at all,
44
+
45
+ 6. By utilizing the --negate option to quickly encrypt a regular file, or decrypt
46
+ an encrypted file with extension .enc
47
+
48
+ 7. By implementing the -t (edit) mode, that opens an encrypted file in your $EDITOR,
49
+ and replaces the encrypted version upon save & exit, optionally creating a backup.
50
+
51
+ 8. By offering the Sym::MagicFile ruby API to easily read encrypted files into memory.
52
+
53
+ Please refer the module documentation available here:
54
+ https://www.rubydoc.info/gems/sym
55
+
7
56
  eof
8
57
  end
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  lib = File.expand_path('../lib', __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'sym/version'
@@ -19,48 +18,50 @@ Gem::Specification.new do |spec|
19
18
  spec.bindir = 'exe'
20
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
20
  spec.require_paths = ['lib']
22
- spec.required_ruby_version = '>= 2.2'
23
- spec.post_install_message = <<-EOF
24
-
25
- Thank you for installing Sym!
26
-
27
- BLOG POST
28
- =========
29
- http://kig.re/2017/03/10/dead-simple-encryption-with-sym.html
30
-
31
- BASH COMPLETION
32
- ===============
33
- To enable bash command line completion and install highly useful
34
- command line BASH wrapper 'symit', please run the following
35
- command after installing the gem. It appends sym's shell completion
36
- wrapper to the file specified in arguments to -B flag.
37
-
38
- sym -B ~/.bash_profile
39
- source ~/.bash_profile
40
- # then:
41
- sym --help
42
- symit --help
43
-
44
- Thank you for using Sym and happy encrypting :)
45
-
46
- @kigster on Github,
47
- @kig on Twitter.
48
-
49
- EOF
21
+ spec.required_ruby_version = '>= 2.3'
22
+ spec.post_install_message = <<~EOF
23
+
24
+ Thank you for installing Sym!
25
+
26
+ BLOG POST
27
+ =========
28
+ http://kig.re/2017/03/10/dead-simple-encryption-with-sym.html
29
+
30
+ BASH COMPLETION
31
+ ===============
32
+ To enable bash command line completion and install highly useful
33
+ command line BASH wrapper 'symit', please run the following
34
+ command after installing the gem. It appends sym's shell completion
35
+ wrapper to the file specified in arguments to -B flag.
36
+
37
+ sym -B ~/.bash_profile
38
+ source ~/.bash_profile
39
+ # then:
40
+ sym --help
41
+ symit --help
42
+
43
+ Thank you for using Sym and happy encrypting :)
44
+
45
+ @kigster on Github,
46
+ @kig on Twitter.
47
+
48
+ EOF
50
49
  spec.add_dependency 'colored2', '~> 3'
51
50
  spec.add_dependency 'slop', '~> 4.3'
52
51
  spec.add_dependency 'activesupport'
53
- spec.add_dependency 'highline', '~> 1.7'
54
- spec.add_dependency 'coin', '~> 0.1.8'
55
- spec.add_dependency 'dalli', '~> 2.7'
52
+ spec.add_dependency 'highline'
53
+ spec.add_dependency 'dalli'
56
54
 
57
- spec.add_development_dependency 'codeclimate-test-reporter', '~> 1.0'
58
- spec.add_development_dependency 'simplecov'
59
- spec.add_development_dependency 'irbtools'
55
+ spec.add_development_dependency 'asciidoctor'
60
56
  spec.add_development_dependency 'aruba'
61
57
  spec.add_development_dependency 'bundler'
58
+ spec.add_development_dependency 'irbtools'
62
59
  spec.add_development_dependency 'rake'
60
+ spec.add_development_dependency 'relaxed-rubocop'
63
61
  spec.add_development_dependency 'rspec', '~> 3'
64
62
  spec.add_development_dependency 'rspec-its'
63
+ spec.add_development_dependency 'rubocop', '0.81.0'
64
+ spec.add_development_dependency 'simplecov'
65
+ spec.add_development_dependency 'codecov'
65
66
  spec.add_development_dependency 'yard'
66
67
  end