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,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