sym 0.1.0 → 2.0.0

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