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,56 @@
1
+ require 'active_support/inflector'
2
+ require 'tsort'
3
+ require 'pp'
4
+ module Sym
5
+ module App
6
+ module Commands
7
+
8
+ class DependencyResolver < Hash
9
+ include TSort
10
+ alias tsort_each_node each_key
11
+
12
+ def tsort_each_child(node, &block)
13
+ fetch(node).each(&block)
14
+ end
15
+ end
16
+
17
+ @dependency = DependencyResolver.new
18
+ @commands = Set.new
19
+
20
+ class << self
21
+ attr_accessor :commands, :dependency
22
+
23
+ def register(command_class)
24
+ self.commands << command_class
25
+ self.dependency[command_class.short_name] ||= []
26
+ end
27
+
28
+ def order(command_class, after)
29
+ self.dependency[command_class.short_name].unshift(after) if after
30
+ self.dependency[command_class.short_name].flatten!
31
+ end
32
+
33
+ def dependencies
34
+ @dependencies ||= self.dependency.tsort
35
+ @dependencies
36
+ end
37
+
38
+ # Sort commands based on the #dependencies array, which itself is sorted
39
+ # based on command dependencies.
40
+ def sorted_commands
41
+ @sorted_commands ||= self.commands.to_a.sort_by{|klass| dependencies.index(klass.short_name) }
42
+ @sorted_commands
43
+ end
44
+
45
+ def find_command_class(opts)
46
+ self.sorted_commands.each do |command_class|
47
+ return command_class if command_class.options_satisfied_by?(opts)
48
+ end
49
+ nil
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ require_dir 'sym/app/commands'
@@ -0,0 +1,77 @@
1
+ require 'sym'
2
+ require 'sym/app'
3
+
4
+ require 'active_support/inflector'
5
+
6
+ module Sym
7
+ module App
8
+ module Commands
9
+ class Command
10
+
11
+ def self.inherited(klass)
12
+ klass.instance_eval do
13
+ class << self
14
+ attr_accessor :required, :incompatible
15
+
16
+ include Sym::App::ShortName
17
+
18
+ def try_after(*dependencies)
19
+ Sym::App::Commands.order(self, dependencies)
20
+ end
21
+
22
+ def required_options(*args)
23
+ self.required ||= Set.new
24
+ required.merge(args) if args
25
+ required
26
+ end
27
+
28
+ def incompatible_options(*args)
29
+ self.incompatible ||= Set.new
30
+ incompatible.merge(args) if args
31
+ incompatible
32
+ end
33
+
34
+ def options_satisfied_by?(opts_hash)
35
+ proc = required_options.find { |option| option.is_a?(Proc) }
36
+ return true if proc && proc.call(opts_hash)
37
+ return false if incompatible_options.any? { |option| opts_hash[option] }
38
+ required_options.to_a.delete_if { |o| o.is_a?(Proc) }.all? { |o|
39
+ o.is_a?(Array) ? o.any? { |opt| opts_hash[opt] } : opts_hash[o]
40
+ }
41
+ end
42
+ end
43
+
44
+ # Register this command with the global list.
45
+ Sym::App::Commands.register klass
46
+ end
47
+ end
48
+
49
+ attr_accessor :application
50
+
51
+ def initialize(application)
52
+ self.application = application
53
+ end
54
+
55
+ def opts
56
+ application.opts
57
+ end
58
+ def opts_hash
59
+ application.opts_hash
60
+ end
61
+
62
+ def key
63
+ @key ||= application.key
64
+ end
65
+
66
+ def execute
67
+ raise Sym::Errors::AbstractMethodCalled.new(:run)
68
+ end
69
+
70
+ def to_s
71
+ "#{self.class.short_name.to_s.bold.yellow}, with options: #{application.args.argv.join(' ').gsub(/--/, '').bold.green}"
72
+ end
73
+
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'command'
2
+ require 'sym/app/keychain'
3
+ module Sym
4
+ module App
5
+ module Commands
6
+ class DeleteKeychainItem < Command
7
+
8
+ required_options :keychain_del
9
+ try_after :generate_key, :open_editor, :encrypt_decrypt
10
+
11
+ def execute
12
+ Sym::App::KeyChain.new(opts[:keychain_del]).delete
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'command'
2
+ module Sym
3
+ module App
4
+ module Commands
5
+ class EncryptDecrypt < Command
6
+ include Sym
7
+
8
+ required_options [ :private_key, :keyfile, :keychain, :interactive ],
9
+ [ :encrypt, :decrypt ],
10
+ [ :file, :string ]
11
+
12
+ try_after :generate_key
13
+
14
+ def execute
15
+ send(application.action, content, application.key)
16
+ end
17
+
18
+ private
19
+
20
+ def content
21
+ @content ||= (opts[:string] || (opts[:file].eql?('-') ? STDIN.read : File.read(opts[:file])))
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ require_relative 'command'
2
+ require 'sym/app/keychain'
3
+ module Sym
4
+ module App
5
+ module Commands
6
+ class GenerateKey < Command
7
+ include Sym
8
+
9
+ required_options :generate
10
+
11
+ def execute
12
+ retries ||= 0
13
+ new_private_key = self.class.create_private_key
14
+ new_private_key = encr_password(new_private_key,
15
+ application.input_handler.new_password) if opts[:password]
16
+
17
+ clipboard_copy(new_private_key) if opts[:copy]
18
+
19
+ Sym::App::KeyChain.new(opts[:keychain], opts).
20
+ add(new_private_key) if opts[:keychain] && Sym::App.is_osx?
21
+
22
+ new_private_key
23
+ rescue Sym::Errors::PasswordsDontMatch, Sym::Errors::PasswordTooShort => e
24
+ STDERR.puts e.message.bold
25
+ retry if (retries += 1) < 3
26
+ end
27
+
28
+ private
29
+
30
+ def clipboard_copy(key)
31
+ require 'clipboard'
32
+ Clipboard.copy(key)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,97 @@
1
+ require 'digest'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'sym'
5
+ require 'sym/errors'
6
+ require_relative 'command'
7
+ module Sym
8
+ module App
9
+ module Commands
10
+ class OpenEditor < Command
11
+ include Sym
12
+
13
+ required_options [ :private_key, :keyfile, :keychain, :interactive ],
14
+ :edit,
15
+ :file
16
+
17
+ try_after :generate_key, :encrypt_decrypt
18
+
19
+ attr_accessor :tempfile
20
+
21
+ def execute
22
+ begin
23
+ self.tempfile = ::Tempfile.new(::Base64.urlsafe_encode64(opts[:file]))
24
+ decrypt_content(self.tempfile)
25
+
26
+ result = process launch_editor
27
+ ensure
28
+ self.tempfile.close if tempfile
29
+ self.tempfile.unlink rescue nil
30
+ end
31
+ result
32
+ end
33
+
34
+ def launch_editor
35
+ system("#{application.editor} #{tempfile.path}")
36
+ end
37
+
38
+ private
39
+
40
+ def decrypt_content(file)
41
+ file.open
42
+ file.write(content)
43
+ file.flush
44
+ end
45
+
46
+ def content
47
+ @content ||= decr(File.read(opts[:file]), key)
48
+ end
49
+
50
+ def timestamp
51
+ @timestamp ||= Time.now.to_a.select { |d| d.is_a?(Fixnum) }.map { |d| '%02d' % d }[0..-3].reverse.join
52
+ end
53
+
54
+ def process(code)
55
+ if code == true
56
+ content_edited = File.read(tempfile.path)
57
+ md5 = ::Base64.encode64(Digest::MD5.new.digest(content))
58
+ md5_edited = ::Base64.encode64(Digest::MD5.new.digest(content_edited))
59
+ return 'No changes have been made.' if md5 == md5_edited
60
+
61
+ FileUtils.cp opts[:file], "#{opts[:file]}.#{timestamp}" if opts[:backup]
62
+
63
+ diff = compute_diff
64
+
65
+ File.open(opts[:file], 'w') { |f| f.write(encr(content_edited, key)) }
66
+
67
+ out = ''
68
+ if opts[:verbose]
69
+ out << "Saved encrypted/compressed content to #{opts[:file].bold.blue}" +
70
+ " (#{File.size(opts[:file]) / 1024}Kb), unencrypted size #{content.length / 1024}Kb."
71
+ out << (opts[:backup] ? ",\nbacked up the last version to #{backup_file.bold.blue}." : '.')
72
+ end
73
+ out << "\n\nDiff:\n#{diff}"
74
+ out
75
+ else
76
+ raise Sym::Errors::EditorExitedAbnormally.new("#{application.editor} exited with #{$<}")
77
+ end
78
+ end
79
+
80
+ # Computes the diff between two unencrypted versions
81
+ def compute_diff
82
+ original_content_file = Tempfile.new(rand(1024).to_s)
83
+ original_content_file.open
84
+ original_content_file.write(content)
85
+ original_content_file.flush
86
+ diff = `diff #{original_content_file.path} #{tempfile.path}`
87
+ diff.gsub!(/> (.*\n)/m, '\1'.green)
88
+ diff.gsub!(/< (.*\n)/m, '\1'.red)
89
+ ensure
90
+ original_content_file.close
91
+ original_content_file.unlink
92
+ diff
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'command'
2
+ require 'sym/app/keychain'
3
+ module Sym
4
+ module App
5
+ module Commands
6
+ class PrintKey < Command
7
+ required_options [ :keychain, :keyfile ]
8
+
9
+ def execute
10
+ self.key
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,76 @@
1
+ require 'colored2'
2
+ require_relative 'command'
3
+ module Sym
4
+ module App
5
+ module Commands
6
+ class ShowExamples < Command
7
+ required_options :examples
8
+ try_after :show_help
9
+
10
+ def execute
11
+ output = []
12
+
13
+ output << example(comment: 'generate a new private key into an environment variable:',
14
+ command: 'export KEY=$(sym -g)',
15
+ echo: 'echo $KEY',
16
+ result: '75ngenJpB6zL47/8Wo7Ne6JN1pnOsqNEcIqblItpfg4='.green)
17
+
18
+ output << example(comment: 'generate a new password-protected key, copy to the clipboard & save to a file',
19
+ command: 'sym -gpc -o ~/.key',
20
+ echo: 'New Password : ' + '••••••••••'.green,
21
+ result: 'Confirm Password : ' + '••••••••••'.green)
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 $KEY -o file.enc',
25
+ echo: 'cat file.enc',
26
+ result: 'Y09MNDUyczU1S0UvelgrLzV0RTYxZz09CkBDMEw4Q0R0TmpnTm9md1QwNUNy%T013PT0K'.green)
27
+
28
+ output << example(comment: 'decrypt a previously encrypted string:',
29
+ command: 'sym -d -s $(cat file.enc) -k $KEY',
30
+ result: 'secret string'.green)
31
+
32
+ output << example(comment: 'encrypt sym.yml and save it to sym.enc:',
33
+ command: 'sym -e -f sym.yml -o sym.enc -k $KEY')
34
+
35
+ output << example(comment: 'decrypt an encrypted file and print it to STDOUT:',
36
+ command: 'sym -df sym.enc -k $KEY')
37
+
38
+ output << example(comment: 'edit an encrypted file in $EDITOR, ask for key, create a backup',
39
+ command: 'sym -tibf ecrets.enc',
40
+ result: '
41
+ Private Key: ••••••••••••••••••••••••••••••••••••••••••••
42
+ Saved encrypted content to sym.enc.
43
+
44
+ Diff:
45
+ 3c3
46
+ '.white.dark + '# (c) 2015 Konstantin Gredeskoul. All rights reserved.'.red.bold + '
47
+ ---' + '
48
+ # (c) 2016 Konstantin Gredeskoul. All rights reserved.'.green.bold)
49
+
50
+
51
+ if Sym::App.is_osx?
52
+ output << example(comment: 'generate a new password-encrypted key, save it to your Keychain:',
53
+ command: 'sym -gpx mykey -o ~/.key')
54
+
55
+ output << example(comment: 'use the new key to encrypt a file:',
56
+ command: 'sym -x mykey -e -f password.txt -o passwords.enc')
57
+
58
+ output << example(comment: 'use the new key to inline-edit the encrypted file:',
59
+ command: 'sym -x mykey -t -f sym.yml')
60
+ end
61
+
62
+ output.flatten.compact.join("\n")
63
+ end
64
+
65
+ def example(comment: nil, command: nil, echo: nil, result: nil)
66
+ out = []
67
+ out << "# #{comment}".white.dark.italic if comment
68
+ out << "#{command}" if command
69
+ out << "#{echo}" if echo
70
+ out << "#{result}" if result
71
+ out << '—'*80
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,16 @@
1
+ require_relative 'command'
2
+ module Sym
3
+ module App
4
+ module Commands
5
+ class ShowHelp < Command
6
+
7
+ required_options :help, ->(opts) { opts.to_hash.keys.all? { |k| !opts[k] } }
8
+ try_after :generate_key, :open_editor, :encrypt_decrypt
9
+
10
+ def execute
11
+ opts.to_s(prefix: ' ' * 2)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,81 @@
1
+ require 'colored2'
2
+ require_relative 'command'
3
+ require_relative '../nlp'
4
+ module Sym
5
+ module App
6
+ module Commands
7
+ class ShowLanguageExamples < Command
8
+ required_options :language
9
+ try_after :show_help
10
+
11
+
12
+
13
+ def execute
14
+ output = []
15
+
16
+ output << Sym::App::NLP::Base.usage
17
+
18
+ output << example(comment: 'generate a new private key and copy to the clipboard but do not print to terminal',
19
+ command: 'sym create new key to clipboard quietly'
20
+ )
21
+
22
+ output << example(comment: 'generate and save to a file a password-protected key, silently',
23
+ command: 'sym create a secure key and save it to "my.key"',
24
+ )
25
+
26
+ output << example(comment: 'encrypt a plain text string with a key, and save the output to a file',
27
+ command: 'sym encrypt string "secret string" using $(cat my.key) save to file.enc')
28
+
29
+ output << example(comment: 'decrypt a previously encrypted string:',
30
+ command: 'sym decrypt string $ENC using $(cat my.key)')
31
+
32
+ output << example(comment: 'encrypt "file.txt" with key from my.key and save it to file.enc',
33
+ command: 'sym encrypt file file.txt with key from my.key and save it to file.enc')
34
+
35
+ output << example(comment: 'decrypt an encrypted file and print it to STDOUT:',
36
+ command: 'sym decrypt file file.enc with key from "my.key"')
37
+
38
+ output << example(comment: 'edit an encrypted file in $EDITOR, ask for key, and create a backup upon save',
39
+ command: 'sym edit file file.enc ask for a key and make a backup',
40
+ )
41
+
42
+ if Sym::App.is_osx?
43
+ output << example(comment: 'generate a new password-encrypted key, save it to your Keychain:',
44
+ command: 'sym create a new protected key store in keychain "my-keychain-key"')
45
+
46
+ output << example(comment: 'print the key stored in the keychain item "my-keychain-key"',
47
+ command: 'sym print keychain "my-keychain-key"')
48
+
49
+ output << example(comment: 'use the new key to encrypt a file:',
50
+ command: 'sym encrypt with keychain "my-keychain-key" file "password.txt" and write to "passwords.enc"')
51
+
52
+ end
53
+
54
+ output.flatten.compact.join("\n")
55
+ end
56
+
57
+ def example(comment: nil, command: nil, echo: nil, result: nil)
58
+ @dict ||= ::Sym::App::NLP::Constants::DICTIONARY.to_a.flatten!
59
+ _command = command.split(' ').map do |w|
60
+ _w = w.to_sym
61
+ if w == 'sym'
62
+ w.italic.yellow
63
+ elsif ::Sym::App::NLP::Constants::STRIPPED.include?(_w)
64
+ w.italic.red
65
+ elsif @dict.include?(_w)
66
+ w.blue
67
+ else
68
+ w
69
+ end
70
+ end.join(' ') if command
71
+ out = []
72
+ out << "# #{comment}".white.dark.italic if comment
73
+ out << "#{_command}" if command
74
+ out << "#{echo}" if echo
75
+ out << "#{result}" if result
76
+ out << (' '*80).dark
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end