sym 2.8.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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