sym 2.3.0 → 2.4.3

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -2
  3. data/README.md +106 -70
  4. data/Rakefile +3 -2
  5. data/bin/sym.completion +6 -2
  6. data/lib/sym.rb +28 -15
  7. data/lib/sym/app.rb +1 -3
  8. data/lib/sym/app/args.rb +5 -1
  9. data/lib/sym/app/cli.rb +54 -6
  10. data/lib/sym/app/cli_slop.rb +35 -23
  11. data/lib/sym/app/commands/base_command.rb +12 -19
  12. data/lib/sym/app/commands/bash_completion.rb +19 -3
  13. data/lib/sym/app/commands/decrypt.rb +1 -1
  14. data/lib/sym/app/commands/encrypt.rb +1 -1
  15. data/lib/sym/app/commands/generate_key.rb +7 -3
  16. data/lib/sym/app/commands/keychain_add_key.rb +7 -2
  17. data/lib/sym/app/commands/open_editor.rb +1 -1
  18. data/lib/sym/app/commands/password_protect_key.rb +9 -4
  19. data/lib/sym/app/commands/print_key.rb +3 -3
  20. data/lib/sym/app/commands/show_examples.rb +25 -17
  21. data/lib/sym/app/commands/show_help.rb +2 -2
  22. data/lib/sym/app/keychain.rb +5 -0
  23. data/lib/sym/app/output/base.rb +3 -7
  24. data/lib/sym/app/output/file.rb +0 -1
  25. data/lib/sym/app/output/noop.rb +2 -1
  26. data/lib/sym/app/password/providers.rb +4 -0
  27. data/lib/sym/app/password/providers/memcached_provider.rb +1 -1
  28. data/lib/sym/app/private_key/base64_decoder.rb +1 -0
  29. data/lib/sym/app/private_key/decryptor.rb +1 -0
  30. data/lib/sym/app/private_key/detector.rb +45 -26
  31. data/lib/sym/app/private_key/handler.rb +20 -25
  32. data/lib/sym/app/private_key/key_source_check.rb +89 -0
  33. data/lib/sym/application.rb +115 -33
  34. data/lib/sym/configuration.rb +1 -0
  35. data/lib/sym/constants.rb +24 -0
  36. data/lib/sym/data/decoder.rb +2 -1
  37. data/lib/sym/data/wrapper_struct.rb +1 -1
  38. data/lib/sym/extensions/stdlib.rb +1 -0
  39. data/lib/sym/version.rb +1 -1
  40. data/sym.gemspec +1 -1
  41. metadata +5 -4
  42. data/lib/sym/encrypted_file.rb +0 -34
@@ -10,7 +10,7 @@ module Sym
10
10
  class OpenEditor < BaseCommand
11
11
  include Sym
12
12
 
13
- required_options [ :private_key, :keyfile, :keychain, :interactive ],
13
+ required_options [ :key, :interactive ],
14
14
  :edit,
15
15
  :file
16
16
 
@@ -5,16 +5,21 @@ module Sym
5
5
  module Commands
6
6
  class PasswordProtectKey < BaseCommand
7
7
 
8
- required_options [:private_key, :keyfile, :keychain, :interactive],
9
- :password
10
-
8
+ required_options %i(key interactive), :password
9
+ incompatible_options %i(examples help version bash_completion)
11
10
  try_after :generate_key, :encrypt, :decrypt
12
11
 
13
12
  def execute
14
13
  retries ||= 0
15
14
 
16
15
  the_key = self.key
17
- the_key = encrypt_password_if_needed(the_key)
16
+
17
+ if opts[:password]
18
+ encrypted_key, password = encrypt_with_password(the_key)
19
+ add_password_to_the_cache(encrypted_key, password)
20
+ the_key = encrypted_key
21
+ end
22
+
18
23
  add_to_keychain_if_needed(the_key)
19
24
 
20
25
  the_key
@@ -4,9 +4,9 @@ module Sym
4
4
  module App
5
5
  module Commands
6
6
  class PrintKey < BaseCommand
7
- required_options [ :keychain, :keyfile ]
8
-
9
- try_after :generate_key, :encrypt, :decrypt, :password_protect_key, :keychain_add_key
7
+ required_options %i(keychain key)
8
+ incompatible_options %i(examples help version bash_completion)
9
+ try_after :show_examples, :generate_key, :encrypt, :decrypt, :password_protect_key, :keychain_add_key
10
10
 
11
11
  def execute
12
12
  self.key
@@ -5,7 +5,7 @@ module Sym
5
5
  module Commands
6
6
  class ShowExamples < BaseCommand
7
7
  required_options :examples
8
- try_after :show_help
8
+ try_after :show_version
9
9
 
10
10
  def execute
11
11
  output = []
@@ -15,28 +15,36 @@ module Sym
15
15
  echo: 'echo $mykey',
16
16
  result: '75ngenJpB6zL47/8Wo7Ne6JN1pnOsqNEcIqblItpfg4='.green)
17
17
 
18
- output << example(comment: 'generate a new password-protected key & save to a file',
19
- command: 'sym -gp -o ~/.key',
18
+ output << example(comment: 'generate a new key with a cached password & save to the default key file',
19
+ command: 'sym -gcpqo ' + Sym.default_key_file,
20
20
  echo: 'New Password : ' + '••••••••••'.green,
21
21
  result: 'Confirm Password : ' + '••••••••••'.green)
22
22
 
23
- output << example(comment: 'encrypt a plain text string with a key, and save the output to a file',
24
- command: 'sym -e -s ' + '"secret string"'.bold.yellow + ' -k $mykey -o file.enc',
25
- echo: 'cat file.enc',
26
- result: 'Y09MNDUyczU1S0UvelgrLzV0RTYxZz09CkBDMEw4Q0R0TmpnTm9md1QwNUNy%T013PT0K'.green)
23
+ output << example(comment: 'encrypt a plain text string with default key file, and immediately decrypt it',
24
+ command: 'sym -es ' + '"secret string"'.bold.yellow + ' | sym -d',
25
+ result: 'secret string'.green)
26
+
27
+ output << example(comment: 'encrypt secrets file using key in the environment, and --negate option:',
28
+ command: 'export PRIVATE_KEY="75ngenJpB6zL47/8Wo7Ne6JN1pnOsqNEcIqblItpfg4="',
29
+ echo: 'sym -ck PRIVATE_KEY -n secrets.yml',
30
+ result: ''.green)
27
31
 
28
- output << example(comment: 'decrypt a previously encrypted string:',
29
- command: 'sym -d -s $(cat file.enc) -k $mykey',
32
+ output << example(comment: 'encrypt a secrets file using the key in the keychain:',
33
+ command: 'sym -gqx keychain.key',
34
+ echo: 'sym -ck keychain.key -n secrets.yml',
30
35
  result: 'secret string'.green)
31
36
 
32
- output << example(comment: 'encrypt sym.yml and save it to sym.enc:',
33
- command: 'sym -e -f sym.yml -o sym.enc -k $mykey')
37
+ output << example(comment: 'encrypt/decrypt sym.yml using the default key file',
38
+ command: 'sym -gcq > ' + Sym.default_key_file,
39
+ echo: 'sym -n secrets.yml',
40
+ result: 'sym -df secrets.yml.enc',
41
+ )
34
42
 
35
43
  output << example(comment: 'decrypt an encrypted file and print it to STDOUT:',
36
- command: 'sym -df sym.enc -k $mykey')
44
+ command: 'sym -ck production.key -df secrets.yml.enc')
37
45
 
38
- output << example(comment: 'edit an encrypted file in $EDITOR, ask for key, create file backup',
39
- command: 'sym -tibf ecrets.enc',
46
+ output << example(comment: 'edit an encrypted file in $EDITOR, use default key file, create file backup',
47
+ command: 'sym -tbf secrets.enc',
40
48
  result: '
41
49
  Private Key: ••••••••••••••••••••••••••••••••••••••••••••
42
50
  Saved encrypted content to sym.enc.
@@ -50,13 +58,13 @@ Diff:
50
58
 
51
59
  if Sym::App.is_osx?
52
60
  output << example(comment: 'generate a new password-encrypted key, save it to your Keychain:',
53
- command: 'sym -gpx mykey -o ~/.key')
61
+ command: 'sym -gpcx staging.key')
54
62
 
55
63
  output << example(comment: 'use the new key to encrypt a file:',
56
- command: 'sym -x mykey -e -f password.txt -o passwords.enc')
64
+ command: 'sym -e -c -k staging.key -n etc/passwords.enc')
57
65
 
58
66
  output << example(comment: 'use the new key to inline-edit the encrypted file:',
59
- command: 'sym -x mykey -t -f sym.yml')
67
+ command: 'sym -k mykey -tf sym.yml.enc')
60
68
  end
61
69
 
62
70
  output.flatten.compact.join("\n")
@@ -4,10 +4,10 @@ module Sym
4
4
  module Commands
5
5
  class ShowHelp < BaseCommand
6
6
 
7
- required_options :help, ->(opts) { opts.to_hash.keys.all? { |k| !opts[k] } }
7
+ required_options :help, ->(opts) { opts.keys.all? { |k| !opts[k] } }
8
8
 
9
9
  def execute
10
- opts.to_s(prefix: ' ' * 2)
10
+ opts_original.to_s(prefix: ' ' * 2)
11
11
  end
12
12
  end
13
13
  end
@@ -13,6 +13,10 @@ module Sym
13
13
  class << self
14
14
  attr_accessor :user, :kind, :sub_section
15
15
 
16
+ def get(value)
17
+ self.new(value).find
18
+ end
19
+
16
20
  def configure
17
21
  yield self
18
22
  end
@@ -35,6 +39,7 @@ module Sym
35
39
  self.key_name = key_name
36
40
  self.opts = opts
37
41
  self.class.validate!
42
+ stderr_off
38
43
  end
39
44
 
40
45
  def add(password)
@@ -4,14 +4,10 @@ module Sym
4
4
  module Output
5
5
  class Base
6
6
 
7
- attr_accessor :cli
7
+ attr_accessor :opts
8
8
 
9
- def initialize(cli)
10
- self.cli = cli
11
- end
12
-
13
- def opts
14
- cli.opts
9
+ def initialize(opts)
10
+ self.opts = opts
15
11
  end
16
12
 
17
13
  @outputs = []
@@ -6,7 +6,6 @@ module Sym
6
6
 
7
7
  required_option :output
8
8
 
9
-
10
9
  def output_proc
11
10
  ->(data) {
12
11
  ::File.open(opts[:output], 'w') { |f| f.write(data) }
@@ -1,4 +1,5 @@
1
- require_relative 'base'
1
+ require 'sym/app/output/base'
2
+
2
3
  module Sym
3
4
  module App
4
5
  module Output
@@ -26,6 +26,10 @@ module Sym
26
26
  provider_from_argument(p, **opts, &block) || detect
27
27
  end
28
28
 
29
+ def provider_list
30
+ registry.keys.map(&:to_s).join(', ')
31
+ end
32
+
29
33
  private
30
34
 
31
35
  def short_name(klass)
@@ -11,7 +11,7 @@ module Sym
11
11
 
12
12
  def initialize(**opts)
13
13
  # disable logging
14
- Dalli.logger = Sym::NIL_LOGGER
14
+ Dalli.logger = Sym::Constants::Log::NIL
15
15
  self.dalli = ::Dalli::Client.new(
16
16
  * Sym::Configuration.config.password_cache_arguments[:memcached][:args],
17
17
  ** Sym::Configuration.config.password_cache_arguments[:memcached][:opts].merge!(opts)
@@ -1,3 +1,4 @@
1
+ require 'base64'
1
2
  module Sym
2
3
  module App
3
4
  module PrivateKey
@@ -1,5 +1,6 @@
1
1
  require 'sym/app/private_key/decryptor'
2
2
  require 'sym/app/password/cache'
3
+ require 'sym/errors'
3
4
  module Sym
4
5
  module App
5
6
  module PrivateKey
@@ -1,42 +1,61 @@
1
+ require 'sym/app/private_key/base64_decoder'
2
+ require 'sym/app/private_key/decryptor'
3
+ require 'sym/app/private_key/key_source_check'
4
+ require 'sym/app/input/handler'
5
+
1
6
  module Sym
2
7
  module App
3
8
  module PrivateKey
4
- class Detector < Struct.new(:opts, :input_handler) # :nodoc:s
5
- @mapping = Hash.new
6
- class << self
7
- attr_reader :mapping
9
+ class Detector < Struct.new(:opts, :input_handler, :password_cache)
10
+ attr_accessor :key, :key_source
8
11
 
9
- def register(argument, proc)
10
- self.mapping[argument] = proc
11
- end
12
+ def initialize(*args)
13
+ super(*args)
14
+ read
15
+ end
16
+
17
+ def read
18
+ return key if key
19
+ self.key, self.key_source = read!
12
20
  end
13
21
 
14
- def key
15
- self.class.mapping.each_pair do |options_key, key_proc|
16
- return key_proc.call(opts[options_key], self) if opts[options_key]
22
+ # Returns the first valid 32-bit key obtained by running the above
23
+ # procs on a given string.
24
+ def read!
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
31
+ end
17
32
  end
18
33
  nil
19
34
  end
20
- end
21
-
22
- Detector.register :private_key,
23
- ->(key, *) { key }
24
35
 
25
- Detector.register :interactive,
26
- ->(*, detector) { detector.input_handler.prompt('Please paste your private key: ', :magenta) }
36
+ private
27
37
 
28
- Detector.register :keychain,
29
- ->(key_name, * ) { KeyChain.new(key_name).find rescue nil }
38
+ def normalize_key(key)
39
+ return nil if key.nil?
40
+ if key && key.length > 45
41
+ key = Decryptor.new(Base64Decoder.new(key).key, input_handler, password_cache).key
42
+ end
43
+ validate(key)
44
+ end
30
45
 
31
- Detector.register :keyfile,
32
- ->(file, *) {
33
- begin
34
- ::File.read(file)
35
- rescue Errno::ENOENT
36
- raise Sym::Errors::FileNotFound.new("Encryption key file #{file} was not found.")
37
- nil
46
+ def validate(key)
47
+ if key
48
+ begin
49
+ decoded = Base64Decoder.new(key).key
50
+ decoded.length == 32 ? key : nil
51
+ rescue
52
+ nil
53
+ end
54
+ else
55
+ nil
56
+ end
38
57
  end
39
- }
58
+ end
40
59
  end
41
60
  end
42
61
  end
@@ -1,42 +1,37 @@
1
- require_relative 'detector'
2
- require_relative 'base64_decoder'
3
- require_relative 'decryptor'
4
- require_relative '../input/handler'
1
+ require 'sym/app/private_key/base64_decoder'
2
+ require 'sym/app/private_key/decryptor'
3
+ require 'sym/app/private_key/detector'
4
+ require 'sym/app/input/handler'
5
+ require 'sym/app/args'
6
+ require 'sym/errors'
5
7
  module Sym
6
8
  module App
7
9
  module PrivateKey
8
10
  # This class figures out what is the private key that is
9
11
  # provided to be used.
10
- class Handler
12
+ class Handler < Struct.new(:opts, :input_handler, :password_cache)
11
13
  include Sym
14
+ attr_accessor :key, :key_source
12
15
 
13
- attr_accessor :opts, :input_handler, :password_cache
14
- attr_writer :key
15
-
16
- def initialize(opts, input_handler, password_cache)
17
- self.opts = opts
18
- self.input_handler = input_handler
19
- self.password_cache = password_cache
16
+ def initialize(*args)
17
+ super(*args)
18
+ self.key, self.key_source = detect_key
20
19
  end
21
20
 
21
+ private
22
22
 
23
- # @return [String] key Private key detected
24
- def key
25
- return @key if @key
26
-
27
- @key = begin
28
- Detector.new(opts, input_handler).key
23
+ def detect_key
24
+ begin
25
+ reader = Detector.new(opts, input_handler, password_cache)
26
+ key = reader.key
27
+ key_source = reader.key_source
29
28
  rescue Sym::Errors::Error => e
30
- if Sym::App::Args.new(opts).specify_key? && key.nil?
29
+ if Sym::App::Args.new(opts.to_h).specify_key? && key.nil?
31
30
  raise e
32
31
  end
33
32
  end
34
-
35
- if @key && @key.length > 45
36
- @key = Decryptor.new(Base64Decoder.new(key).key, input_handler, password_cache).key
37
- end
38
-
39
- @key
33
+ key = Decryptor.new(Base64Decoder.new(key).key, input_handler, password_cache).key if key && key.length > 45
34
+ return key ? [key, key_source] : [nil, nil]
40
35
  end
41
36
  end
42
37
  end
@@ -0,0 +1,89 @@
1
+ require 'sym/app/private_key/base64_decoder'
2
+ require 'sym/app/private_key/decryptor'
3
+ require 'sym/app/input/handler'
4
+ require 'base64'
5
+
6
+ module Sym
7
+ module App
8
+ module PrivateKey
9
+ class KeySourceResult < Struct.new(:name, :reducted, :input, :key)
10
+ def to_s
11
+ "#{name}://#{reducted ? '[reducted]' : input}"
12
+ end
13
+ end
14
+
15
+ class KeySourceCheck
16
+ attr_accessor :name, :reducted, :input, :output
17
+
18
+ def initialize(name:,
19
+ reducted: false,
20
+ input: ->(detector) { detector.opts[:key] },
21
+ output:)
22
+
23
+ self.name = name
24
+ self.reducted = reducted
25
+ self.input = input
26
+ self.output = output
27
+ end
28
+
29
+ def detect(detector)
30
+ input_value = input.call(detector)
31
+ return nil unless input_value
32
+ KeySourceResult.new(name,
33
+ reducted,
34
+ input_value,
35
+ output.call(detector, input_value))
36
+ end
37
+
38
+ CHECKS = [
39
+ # Order of registration is important for auto-detect feature;
40
+ # First proc is tried before the last; first one to succeed wins.
41
+ KeySourceCheck.new(
42
+ name: :interactive,
43
+ reducted: true,
44
+ input: ->(detector) { detector.opts[:interactive] },
45
+ output: ->(detector, *) { detector.input_handler.prompt('Please paste your private key: ', :magenta) }
46
+ ),
47
+
48
+ KeySourceCheck.new(
49
+ name: :file,
50
+ output: ->(*, value) { ::File.read(value).chomp if value && File.exist?(value) }
51
+ ),
52
+
53
+ KeySourceCheck.new(
54
+ name: :keychain,
55
+ output: ->(*, value) { KeyChain.get(value) if value }
56
+ ),
57
+
58
+ KeySourceCheck.new(
59
+ name: :string,
60
+ reducted: true,
61
+ output: ->(*, value) do
62
+ decoded = begin
63
+ Base64Decoder.new(value).key
64
+ rescue
65
+ nil
66
+ end
67
+ value if decoded
68
+ end
69
+ ),
70
+
71
+ KeySourceCheck.new(
72
+ name: :env,
73
+ output: ->(*, value) { ENV[value] if value =~ /^[a-zA-Z0-9_]+$/ }
74
+ ),
75
+
76
+ KeySourceCheck.new(
77
+ name: :default_file,
78
+ input: ->(*) { Sym.default_key_file if Sym.default_key? },
79
+ output: ->(detector, *) {
80
+ key_provided_by = %i(key interactive) &
81
+ detector.opts.to_hash.keys.select { |k| detector.opts[k] }
82
+ Sym.default_key if key_provided_by.empty?
83
+ }
84
+ )
85
+ ]
86
+ end
87
+ end
88
+ end
89
+ end