sym 0.1.0 → 2.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +25 -0
  3. data/.document +2 -0
  4. data/.gitignore +6 -2
  5. data/.rspec +1 -1
  6. data/.rubocop.yml +1156 -0
  7. data/.travis.yml +10 -2
  8. data/.yardopts +5 -0
  9. data/Gemfile +2 -0
  10. data/LICENSE +22 -0
  11. data/MANAGING-KEYS.md +67 -0
  12. data/README.md +444 -12
  13. data/Rakefile +10 -2
  14. data/bin/sym.bash-completion +24 -0
  15. data/exe/keychain +38 -0
  16. data/exe/sym +20 -0
  17. data/lib/sym.rb +110 -2
  18. data/lib/sym/app.rb +56 -0
  19. data/lib/sym/app/args.rb +42 -0
  20. data/lib/sym/app/cli.rb +192 -0
  21. data/lib/sym/app/commands.rb +56 -0
  22. data/lib/sym/app/commands/command.rb +77 -0
  23. data/lib/sym/app/commands/delete_keychain_item.rb +17 -0
  24. data/lib/sym/app/commands/encrypt_decrypt.rb +26 -0
  25. data/lib/sym/app/commands/generate_key.rb +37 -0
  26. data/lib/sym/app/commands/open_editor.rb +97 -0
  27. data/lib/sym/app/commands/print_key.rb +15 -0
  28. data/lib/sym/app/commands/show_examples.rb +76 -0
  29. data/lib/sym/app/commands/show_help.rb +16 -0
  30. data/lib/sym/app/commands/show_language_examples.rb +81 -0
  31. data/lib/sym/app/commands/show_version.rb +14 -0
  32. data/lib/sym/app/input/handler.rb +41 -0
  33. data/lib/sym/app/keychain.rb +135 -0
  34. data/lib/sym/app/nlp.rb +18 -0
  35. data/lib/sym/app/nlp/constants.rb +32 -0
  36. data/lib/sym/app/nlp/translator.rb +61 -0
  37. data/lib/sym/app/nlp/usage.rb +72 -0
  38. data/lib/sym/app/output.rb +15 -0
  39. data/lib/sym/app/output/base.rb +61 -0
  40. data/lib/sym/app/output/file.rb +18 -0
  41. data/lib/sym/app/output/noop.rb +14 -0
  42. data/lib/sym/app/output/stdout.rb +13 -0
  43. data/lib/sym/app/password/cache.rb +63 -0
  44. data/lib/sym/app/private_key/base64_decoder.rb +17 -0
  45. data/lib/sym/app/private_key/decryptor.rb +71 -0
  46. data/lib/sym/app/private_key/detector.rb +42 -0
  47. data/lib/sym/app/private_key/handler.rb +44 -0
  48. data/lib/sym/app/short_name.rb +10 -0
  49. data/lib/sym/application.rb +114 -0
  50. data/lib/sym/cipher_handler.rb +46 -0
  51. data/lib/sym/configuration.rb +39 -0
  52. data/lib/sym/data.rb +23 -0
  53. data/lib/sym/data/decoder.rb +28 -0
  54. data/lib/sym/data/encoder.rb +24 -0
  55. data/lib/sym/data/wrapper_struct.rb +43 -0
  56. data/lib/sym/encrypted_file.rb +34 -0
  57. data/lib/sym/errors.rb +37 -0
  58. data/lib/sym/extensions/class_methods.rb +12 -0
  59. data/lib/sym/extensions/instance_methods.rb +114 -0
  60. data/lib/sym/version.rb +1 -1
  61. data/sym.gemspec +34 -15
  62. metadata +224 -9
@@ -0,0 +1,14 @@
1
+ require_relative 'command'
2
+ module Sym
3
+ module App
4
+ module Commands
5
+ class ShowVersion < Command
6
+ required_options :version
7
+ try_after :show_help
8
+ def execute
9
+ "sym (version #{Sym::VERSION})"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ require 'sym/errors'
2
+
3
+ module Sym
4
+ module App
5
+ module Input
6
+ class Handler
7
+
8
+ def ask
9
+ retries ||= 0
10
+ prompt('Password: ', :green)
11
+ rescue ::OpenSSL::Cipher::CipherError
12
+ STDERR.puts 'Invalid password. Please try again.'
13
+ retry if (retries += 1) < 3
14
+ nil
15
+ end
16
+
17
+ def puts(*args)
18
+ STDERR.puts args
19
+ end
20
+
21
+ def prompt(message, color)
22
+ HighLine.new(STDIN, STDERR).ask(message.bold) { |q| q.echo = '•'.send(color) }
23
+ end
24
+
25
+ def new_password
26
+ password = prompt('New Password : ', :blue)
27
+
28
+ raise Sym::Errors::PasswordTooShort.new(
29
+ 'Minimum length is 7 characters.') if password.length < 7
30
+
31
+ password_confirm = prompt('Confirm Password : ', :blue)
32
+
33
+ raise Sym::Errors::PasswordsDontMatch.new(
34
+ 'The passwords you entered do not match.') if password != password_confirm
35
+
36
+ password
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,135 @@
1
+ require 'sym'
2
+ require 'sym/app'
3
+ require 'sym/errors'
4
+
5
+
6
+ module Sym
7
+ module App
8
+ #
9
+ # This class forms and shells several commands that wrap Mac OS-X +security+ command.
10
+ # They provide access to storing generic passwords in the KeyChain Access.
11
+ #
12
+ class KeyChain
13
+ class << self
14
+ attr_accessor :user, :kind, :sub_section
15
+
16
+ def configure
17
+ yield self
18
+ end
19
+
20
+ def validate!
21
+ raise ArgumentError.new(
22
+ 'User is not defined. Either set $USER in environment, or directly on the class.') unless self.user
23
+ end
24
+ end
25
+
26
+ configure do
27
+ self.kind = 'sym'
28
+ self.user = ENV['USER']
29
+ self.sub_section = 'generic-password'
30
+ end
31
+
32
+ attr_accessor :key_name, :opts, :stderr_disabled
33
+
34
+ def initialize(key_name, opts = {})
35
+ self.key_name = key_name
36
+ self.opts = opts
37
+ self.class.validate!
38
+ end
39
+
40
+ def add(password)
41
+ execute command(:add, "-U -w '#{password}' ")
42
+ end
43
+
44
+ def find
45
+ execute command(:find, ' -g -w ')
46
+ end
47
+
48
+ def delete
49
+ execute command(:delete)
50
+ end
51
+
52
+ def execute(command)
53
+ command += ' 2>/dev/null' if stderr_disabled
54
+ puts "> #{command.yellow.green}" if opts[:verbose]
55
+ output = `#{command}`
56
+ result = $?
57
+ raise Sym::Errors::KeyChainCommandError.new("Command error: #{result}, command: #{command}") unless result.success?
58
+ output.chomp
59
+ rescue Errno::ENOENT => e
60
+ raise Sym::Errors::KeyChainCommandError.new("Command error: #{e.message}, command: #{command}")
61
+ end
62
+
63
+ def stderr_off
64
+ self.stderr_disabled = true
65
+ end
66
+
67
+ def stderr_on
68
+ self.stderr_disabled = false
69
+ end
70
+
71
+ private
72
+
73
+ def command(action, extras = nil)
74
+ out = base_command(action)
75
+ out << extras if extras
76
+ out = out.join
77
+ # Do not actually ever run these commands on non MacOSX
78
+ out = "echo Run this –\"#{out}\", on #{Sym::App.this_os}?\nAre you sure?" unless Sym::App.is_osx?
79
+ out
80
+ end
81
+
82
+ def base_command(action)
83
+ [
84
+ "security #{action}-#{self.class.sub_section} ",
85
+ "-a '#{self.class.user}' ",
86
+ "-D '#{self.class.kind}' ",
87
+ "-s '#{self.key_name}' "
88
+ ]
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+
95
+ #
96
+ # Usage: add-generic-password [-a account] [-s service] [-w password] [options...] [-A|-T appPath] [keychain]
97
+ # -a Specify account name (required)
98
+ # -c Specify item creator (optional four-character code)
99
+ # -C Specify item type (optional four-character code)
100
+ # -D Specify kind (default is "application password")
101
+ # -G Specify generic attribute (optional)
102
+ # -j Specify comment string (optional)
103
+ # -l Specify label (if omitted, service name is used as default label)
104
+ # -s Specify service name (required)
105
+ # -p Specify password to be added (legacy option, equivalent to -w)
106
+ # -w Specify password to be added
107
+ # -A Allow any application to access this item without warning (insecure, not recommended!)
108
+ # -T Specify an application which may access this item (multiple -T options are allowed)
109
+ # -U Update item if it already exists (if omitted, the item cannot already exist)
110
+ #
111
+ # Usage: find-generic-password [-a account] [-s service] [options...] [-g] [keychain...]
112
+ # -a Match "account" string
113
+ # -c Match "creator" (four-character code)
114
+ # -C Match "type" (four-character code)
115
+ # -D Match "kind" string
116
+ # -G Match "value" string (generic attribute)
117
+ # -j Match "comment" string
118
+ # -l Match "label" string
119
+ # -s Match "service" string
120
+ # -g Display the password for the item found
121
+ # -w Display only the password on stdout
122
+ # If no keychains are specified to search, the default search list is used.
123
+ # Find a generic password item.
124
+ #
125
+ # Usage: delete-generic-password [-a account] [-s service] [options...] [keychain...]
126
+ # -a Match "account" string
127
+ # -c Match "creator" (four-character code)
128
+ # -C Match "type" (four-character code)
129
+ # -D Match "kind" string
130
+ # -G Match "value" string (generic attribute)
131
+ # -j Match "comment" string
132
+ # -l Match "label" string
133
+ # -s Match "service" string
134
+ # If no keychains are specified to search, the default search list is used.
135
+ # Delete a generic password item.
@@ -0,0 +1,18 @@
1
+ require_relative 'cli'
2
+ require_relative 'nlp/constants'
3
+ require_relative 'nlp/usage'
4
+ require_relative 'nlp/translator'
5
+ module Sym
6
+ module App
7
+ # sym generate key to the clipboard and keychain
8
+ # sym encrypt file 'hello' using $key [to output.enc]
9
+ # sym edit 'passwords.enc' using $key
10
+ # sym decrypt /etc/secrets encrypted with $key save to ./secrets
11
+ # sym encrypt file $input with keychain $item
12
+ module NLP
13
+ class Base
14
+ extend Usage
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ module Sym
2
+ module App
3
+ module NLP
4
+ module Constants
5
+ STRIPPED = %i(and a the it item to key with about for of new make store in print)
6
+
7
+ DICTIONARY = {
8
+ # option (Slop)
9
+ # list of english words that map to it
10
+ :copy => [:clipboard],
11
+ :decrypt => [:unlock],
12
+ :edit => [:open],
13
+ :encrypt => [:lock],
14
+ :backup => [],
15
+ :keychain => [],
16
+ :file => [:read],
17
+ :generate => [:create],
18
+ :interactive => [:ask, :enter, :type],
19
+ :keyfile => [:from],
20
+ :output => [:save, :write],
21
+ :private_key => [:using, :private],
22
+ :string => [:value],
23
+ :quiet => [:silently, :quietly, :silent, :sym],
24
+ :password => [:secure, :secured, :protected]
25
+ }
26
+
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,61 @@
1
+ require_relative 'constants'
2
+ module Sym
3
+ module App
4
+ module NLP
5
+ class Translator
6
+
7
+ attr_accessor :argv, :translated_argv, :opts
8
+
9
+ def initialize(argv)
10
+ self.argv = argv
11
+ self.opts = CLI.new(%w(-E)).opts.to_hash
12
+ self.translated_argv = []
13
+ end
14
+
15
+ def dict
16
+ ::Sym::App::NLP::Constants::DICTIONARY
17
+ end
18
+
19
+ def stripped
20
+ ::Sym::App::NLP::Constants::STRIPPED
21
+ end
22
+
23
+ def translate
24
+ self.translated_argv = argv.map do |value|
25
+ nlp_argument = value.to_sym
26
+ arg = nil
27
+ arg ||= dict.keys.find do |key|
28
+ dict[key].include?(nlp_argument) || key == nlp_argument
29
+ end
30
+ arg ||= nlp_argument
31
+
32
+ if stripped.include?(arg)
33
+ # nada
34
+ elsif opts.to_hash.key?(arg)
35
+ '--' + "#{arg.to_s.gsub(/_/, '-')}"
36
+ else
37
+ arg.to_s
38
+ end
39
+ end.compact
40
+
41
+ counts = {}
42
+ translated_argv.each{ |arg| counts.key?(arg) ? counts[arg] += 1 : counts[arg] = 1 }
43
+ translated_argv.delete_if{ |arg| counts[arg] > 1 }
44
+ self
45
+ end
46
+
47
+ def and
48
+ translate if translated_argv.empty?
49
+ if self.translated_argv.include?('--verbose')
50
+ STDERR.puts 'Original arguments: '.dark + "#{argv.join(' ').green}"
51
+ STDERR.puts ' Translated argv: '.dark + "#{translated_argv.join(' ').blue}"
52
+ end
53
+ ::Sym::App::CLI.new(self.translated_argv)
54
+ end
55
+
56
+ alias_method :application, :and
57
+
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,72 @@
1
+ require 'pp'
2
+ module Sym
3
+ module App
4
+ # sym generate key to the clipboard and keychain
5
+ # sym encrypt file 'hello' using $key [to output.enc]
6
+ # sym edit 'passwords.enc' using $key
7
+ # sym decrypt /etc/secrets encrypted with $key save to ./secrets
8
+ # sym encrypt file $input with keychain $item
9
+ module NLP
10
+ module Usage
11
+
12
+ def usage
13
+ out = ''
14
+ out << %Q`
15
+ #{header('Natural Language Processing')}
16
+
17
+ #{'When '.dark.normal}#{'sym'.bold.blue} #{'is invoked, and the first argument does not begin with a dash,
18
+ then the the NLP (natural language processing) Translator is invoked.
19
+ The Translator is based on a very simple algorithm:
20
+
21
+ * ignore any of the words tagged STRIPPED. These are the ambiguous words,
22
+ or words with duplicate meaning.
23
+
24
+ * map the remaining arguments to regular double-dashed options using the DICTIONARY
25
+
26
+ * words that are a direct match for a --option are automatically double-dashed
27
+
28
+ * remaining words are left as is (these would be file names, key names, etc).
29
+
30
+ * finally, the resulting "new" command line is parsed with regular options.
31
+
32
+ * When arguments include "verbose", NLP system will print "before" and "after"
33
+ of the arguments, so that any issues can be debugged and corrected.
34
+
35
+ '.dark.normal}
36
+
37
+ #{header('Currently ignored words:')}
38
+ #{Constants::STRIPPED.join(', ').red.italic}
39
+
40
+ #{header('Regular Word Mapping')}
41
+ #{Constants::DICTIONARY.pretty_inspect.split(/\n/).map do |line|
42
+ line.gsub(
43
+ /[\:\}\,\[\]]/, ''
44
+ ).gsub(
45
+ /[ {](\w+)=>([^\n]*)/, '\2|\1'
46
+ )
47
+ end.map { |line| convert_dictionary(*line.split('|')) }.join}
48
+
49
+ #{header('Examples')}
50
+ `
51
+ out
52
+ end
53
+
54
+ def convert_dictionary(left = '', right = '')
55
+ [
56
+ sprintf('%35.35s', left.gsub(/ /, ' ')).italic.yellow,
57
+ ' ───────➤ '.dark,
58
+ sprintf('--%-20.20s', right).blue,
59
+
60
+ "\n"
61
+ ].join
62
+ end
63
+
64
+ private
65
+ def header(title)
66
+ title.upcase.bold.underlined
67
+ end
68
+
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,15 @@
1
+ require 'sym/app/output/base'
2
+ require 'sym/app/output/file'
3
+ require 'sym/app/output/stdout'
4
+ require 'sym/app/output/noop'
5
+
6
+ module Sym
7
+ module App
8
+ module Output
9
+ def self.outputs
10
+ Sym::App::Output::Base.options_to_outputs
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,61 @@
1
+ require 'sym/app/short_name'
2
+ module Sym
3
+ module App
4
+ module Output
5
+ class Base
6
+
7
+ attr_accessor :cli
8
+
9
+ def initialize(cli)
10
+ self.cli = cli
11
+ end
12
+
13
+ def opts
14
+ cli.opts
15
+ end
16
+
17
+ @outputs = []
18
+ class << self
19
+ attr_accessor :outputs
20
+
21
+ def append(output_klass)
22
+ outputs << output_klass if outputs.is_a?(Array)
23
+ raise "Cannot append #{output_class} after #outputs has been converted to a Hash" \
24
+ unless outputs.is_a?(Array)
25
+ end
26
+
27
+ def map_outputs!
28
+ klasses = self.outputs
29
+ self.outputs = Hash.new
30
+ klasses.each { |k| self.outputs[k.required_option] = k }
31
+ outputs
32
+ end
33
+
34
+ def options_to_outputs
35
+ map_outputs! if outputs.is_a?(Array)
36
+ outputs
37
+ end
38
+ end
39
+
40
+ def self.inherited(klass)
41
+ klass.instance_eval do
42
+ class << self
43
+ attr_writer :required_option
44
+ end
45
+
46
+ klass.required_option = nil
47
+
48
+ class << self
49
+ def required_option(_option = nil)
50
+ self.required_option = _option if _option
51
+ @required_option
52
+ end
53
+ end
54
+ end
55
+ klass.extend(Sym::App::ShortName)
56
+ Sym::App::Output::Base.append klass
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end