shhh 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +25 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +13 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/MANAGING-KEYS.md +67 -0
- data/README.md +328 -0
- data/Rakefile +13 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/keychain +38 -0
- data/exe/shhh +8 -0
- data/lib/shhh/app/args.rb +17 -0
- data/lib/shhh/app/cli.rb +150 -0
- data/lib/shhh/app/commands/command.rb +68 -0
- data/lib/shhh/app/commands/delete_keychain_item.rb +17 -0
- data/lib/shhh/app/commands/encrypt_decrypt.rb +26 -0
- data/lib/shhh/app/commands/generate_key.rb +41 -0
- data/lib/shhh/app/commands/open_editor.rb +96 -0
- data/lib/shhh/app/commands/print_key.rb +18 -0
- data/lib/shhh/app/commands/show_examples.rb +64 -0
- data/lib/shhh/app/commands/show_help.rb +16 -0
- data/lib/shhh/app/commands/show_version.rb +14 -0
- data/lib/shhh/app/commands.rb +55 -0
- data/lib/shhh/app/input/handler.rb +35 -0
- data/lib/shhh/app/keychain.rb +135 -0
- data/lib/shhh/app/output/file.rb +23 -0
- data/lib/shhh/app/output/stdout.rb +11 -0
- data/lib/shhh/app/private_key/base64_decoder.rb +17 -0
- data/lib/shhh/app/private_key/decryptor.rb +50 -0
- data/lib/shhh/app/private_key/detector.rb +34 -0
- data/lib/shhh/app/private_key/handler.rb +34 -0
- data/lib/shhh/app.rb +45 -0
- data/lib/shhh/cipher_handler.rb +45 -0
- data/lib/shhh/configuration.rb +23 -0
- data/lib/shhh/data/decoder.rb +28 -0
- data/lib/shhh/data/encoder.rb +24 -0
- data/lib/shhh/data/wrapper_struct.rb +43 -0
- data/lib/shhh/data.rb +23 -0
- data/lib/shhh/errors.rb +27 -0
- data/lib/shhh/extensions/class_methods.rb +12 -0
- data/lib/shhh/extensions/instance_methods.rb +114 -0
- data/lib/shhh/version.rb +3 -0
- data/lib/shhh.rb +73 -0
- data/shhh.gemspec +33 -0
- metadata +249 -0
data/lib/shhh/app/cli.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'slop'
|
3
|
+
require 'shhh'
|
4
|
+
require 'colored2'
|
5
|
+
require 'yaml'
|
6
|
+
require 'openssl'
|
7
|
+
require 'shhh/app'
|
8
|
+
require 'shhh/errors'
|
9
|
+
require 'shhh/app/commands'
|
10
|
+
require 'shhh/app/keychain'
|
11
|
+
require 'shhh/app/private_key/handler'
|
12
|
+
require 'highline'
|
13
|
+
|
14
|
+
require_relative 'output/file'
|
15
|
+
require_relative 'output/stdout'
|
16
|
+
|
17
|
+
module Shhh
|
18
|
+
module App
|
19
|
+
class CLI
|
20
|
+
|
21
|
+
attr_accessor :opts, :output_proc, :print_proc, :write_proc,
|
22
|
+
:action, :password, :key
|
23
|
+
|
24
|
+
def initialize(argv)
|
25
|
+
begin
|
26
|
+
self.opts = parse(argv.dup)
|
27
|
+
rescue StandardError => e
|
28
|
+
error exception: e
|
29
|
+
return
|
30
|
+
end
|
31
|
+
configure_color(argv)
|
32
|
+
select_output_stream
|
33
|
+
self.action = { opts[:encrypt] => :encr, opts[:decrypt] => :decr }[true]
|
34
|
+
end
|
35
|
+
|
36
|
+
def run
|
37
|
+
return Shhh::App.exit_code if Shhh::App.exit_code != 0
|
38
|
+
|
39
|
+
self.key = PrivateKey::Handler.new(self.opts).key unless opts[:generate]
|
40
|
+
|
41
|
+
return self.output_proc.call(command.run) if command
|
42
|
+
|
43
|
+
# command was not found. Reset output to printing, and return an error.
|
44
|
+
self.output_proc = print_proc
|
45
|
+
command_not_found_error!
|
46
|
+
|
47
|
+
rescue ::OpenSSL::Cipher::CipherError => e
|
48
|
+
error type: 'Cipher Error',
|
49
|
+
details: e.message,
|
50
|
+
reason: 'Perhaps either the secret is invalid, or encrypted data is corrupt.',
|
51
|
+
exception: e
|
52
|
+
|
53
|
+
rescue Shhh::Errors::InvalidEncodingPrivateKey => e
|
54
|
+
error type: 'Private Key Error',
|
55
|
+
details: 'Private key does not appear to be properly encoded. ',
|
56
|
+
reason: (opts[:password] ? nil : 'Perhaps the key is password-protected?'),
|
57
|
+
exception: e
|
58
|
+
|
59
|
+
rescue Shhh::Errors::Error => e
|
60
|
+
error type: 'Error',
|
61
|
+
details: e.message,
|
62
|
+
exception: e
|
63
|
+
|
64
|
+
rescue StandardError => e
|
65
|
+
error exception: e
|
66
|
+
end
|
67
|
+
|
68
|
+
def error(hash)
|
69
|
+
Shhh::App.error(hash.merge(config: (opts ? opts.to_hash : {})))
|
70
|
+
end
|
71
|
+
|
72
|
+
def editor
|
73
|
+
ENV['EDITOR'] || '/bin/vi'
|
74
|
+
end
|
75
|
+
|
76
|
+
def command
|
77
|
+
@command_class ||= Shhh::App::Commands.find_command_class(opts)
|
78
|
+
@command ||= @command_class.new(self) if @command_class
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def select_output_stream
|
84
|
+
self.print_proc = Shhh::App::Output::Stdout.new(self).output_proc
|
85
|
+
self.write_proc = Shhh::App::Output::File.new(self).output_proc
|
86
|
+
self.output_proc = opts[:output] ? self.write_proc : self.print_proc
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
def configure_color(argv)
|
91
|
+
if opts[:no_color]
|
92
|
+
Colored2.disable! # reparse options without the colors to create new help msg
|
93
|
+
self.opts = parse(argv.dup)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def command_not_found_error!
|
98
|
+
if key
|
99
|
+
h = opts.to_hash
|
100
|
+
supplied_opts = h.keys.select { |k| h[k] }.join(', ')
|
101
|
+
error type: 'Options Error',
|
102
|
+
details: 'Unable to determined what command to run',
|
103
|
+
reason: "You provided the following options: #{supplied_opts.bold.yellow}"
|
104
|
+
output_proc.call(opts.to_s)
|
105
|
+
else
|
106
|
+
raise Shhh::Errors::NoPrivateKeyFound.new('Private key is required')
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def parse(arguments)
|
111
|
+
Slop.parse(arguments) do |o|
|
112
|
+
o.banner = 'Usage:'.bold.yellow
|
113
|
+
o.separator ' shhh [options]'.bold.green
|
114
|
+
o.separator 'Modes:'.bold.yellow
|
115
|
+
o.bool '-h', '--help', ' show help'
|
116
|
+
o.bool '-d', '--decrypt', ' decrypt mode'
|
117
|
+
o.bool '-t', '--edit', ' decrypt, open an encr. file in ' + editor
|
118
|
+
o.separator 'Create a private key:'.bold.yellow
|
119
|
+
o.bool '-g', '--generate', ' generate a new private key'
|
120
|
+
o.bool '-p', '--password', ' encrypt the key with a password'
|
121
|
+
o.bool '-c', '--copy', ' copy the new key to the clipboard'
|
122
|
+
o.separator 'Provide a private key:'.bold.yellow
|
123
|
+
o.bool '-i', '--interactive', ' Paste or type the key interactively'
|
124
|
+
o.string '-k', '--private-key', '[key] '.bold.blue + ' private key as a string'
|
125
|
+
o.string '-K', '--keyfile', '[key-file]'.bold.blue + ' private key from a file'
|
126
|
+
if Shhh::App.is_osx?
|
127
|
+
o.string '-x', '--keychain', '[key-name] '.bold.blue + 'private key to/from a password entry'
|
128
|
+
o.string '--keychain-del', '[key-name] '.bold.blue + 'delete keychain entry with that name'
|
129
|
+
end
|
130
|
+
o.separator 'Data:'.bold.yellow
|
131
|
+
o.string '-s', '--string', '[string]'.bold.blue + ' specify a string to encrypt/decrypt'
|
132
|
+
o.string '-f', '--file', '[file] '.bold.blue + ' filename to read from'
|
133
|
+
o.string '-o', '--output', '[file] '.bold.blue + ' filename to write to'
|
134
|
+
o.bool '-b', '--backup', ' create a backup file in the edit mode'
|
135
|
+
o.separator 'Flags:'.bold.yellow
|
136
|
+
o.bool '-v', '--verbose', ' show additional information'
|
137
|
+
o.bool '-T', '--trace', ' print a backtrace of any errors'
|
138
|
+
o.bool '-E', '--examples', ' show several examples'
|
139
|
+
o.bool '-V', '--version', ' print library version'
|
140
|
+
o.bool '-N', '--no-color', ' disable color output'
|
141
|
+
o.bool '-e', '--encrypt', ' encrypt mode'
|
142
|
+
o.separator ''
|
143
|
+
end
|
144
|
+
rescue StandardError => e
|
145
|
+
error exception: e
|
146
|
+
raise(e)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
module Shhh
|
3
|
+
module App
|
4
|
+
module Commands
|
5
|
+
class Command
|
6
|
+
|
7
|
+
def self.inherited(klass)
|
8
|
+
klass.instance_eval do
|
9
|
+
class << self
|
10
|
+
attr_accessor :required, :incompatible
|
11
|
+
|
12
|
+
def try_after(*dependencies)
|
13
|
+
Shhh::App::Commands.order(self, dependencies)
|
14
|
+
end
|
15
|
+
|
16
|
+
def required_options(*args)
|
17
|
+
self.required ||= Set.new
|
18
|
+
required.merge(args) if args
|
19
|
+
required
|
20
|
+
end
|
21
|
+
|
22
|
+
def incompatible_options(*args)
|
23
|
+
self.incompatible ||= Set.new
|
24
|
+
incompatible.merge(args) if args
|
25
|
+
incompatible
|
26
|
+
end
|
27
|
+
|
28
|
+
def short_name
|
29
|
+
name.split(/::/)[-1].underscore.to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
def options_satisfied_by?(opts_hash)
|
33
|
+
proc = required_options.find { |option| option.is_a?(Proc) }
|
34
|
+
return true if proc && proc.call(opts_hash)
|
35
|
+
return false if incompatible_options.any? { |option| opts_hash[option] }
|
36
|
+
required_options.to_a.delete_if { |o| o.is_a?(Proc) }.all? { |o|
|
37
|
+
o.is_a?(Array) ? o.any? { |opt| opts_hash[opt] } : opts_hash[o]
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Register this command with the global list.
|
43
|
+
Shhh::App::Commands.register klass
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_accessor :cli
|
48
|
+
|
49
|
+
def initialize(cli)
|
50
|
+
self.cli = cli
|
51
|
+
end
|
52
|
+
|
53
|
+
def opts
|
54
|
+
cli.opts
|
55
|
+
end
|
56
|
+
|
57
|
+
def key
|
58
|
+
@key ||= cli.key
|
59
|
+
end
|
60
|
+
|
61
|
+
def run
|
62
|
+
raise Shhh::Errors::AbstractMethodCalled.new(:run)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
require 'shhh/app/keychain'
|
3
|
+
module Shhh
|
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 run
|
12
|
+
Shhh::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 Shhh
|
3
|
+
module App
|
4
|
+
module Commands
|
5
|
+
class EncryptDecrypt < Command
|
6
|
+
include Shhh
|
7
|
+
|
8
|
+
required_options [ :private_key, :keyfile, :keychain, :interactive ],
|
9
|
+
[ :encrypt, :decrypt ],
|
10
|
+
[ :file, :string ]
|
11
|
+
|
12
|
+
try_after :generate_key
|
13
|
+
|
14
|
+
def run
|
15
|
+
send(cli.action, content, cli.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,41 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
require 'shhh/app/keychain'
|
3
|
+
module Shhh
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class GenerateKey < Command
|
7
|
+
include Shhh
|
8
|
+
|
9
|
+
required_options :generate
|
10
|
+
|
11
|
+
def run
|
12
|
+
retries ||= 0
|
13
|
+
new_private_key = self.class.create_private_key
|
14
|
+
|
15
|
+
if opts[:password]
|
16
|
+
new_private_key = encr_password(new_private_key,
|
17
|
+
Shhh::App::Input::Handler.new_password)
|
18
|
+
end
|
19
|
+
|
20
|
+
clipboard_copy(new_private_key) if opts[:copy]
|
21
|
+
|
22
|
+
if opts[:keychain] && Shhh::App.is_osx?
|
23
|
+
Shhh::App::KeyChain.new(opts[:keychain]).add(new_private_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
new_private_key
|
27
|
+
rescue Shhh::Errors::PasswordsDontMatch, Shhh::Errors::PasswordTooShort => e
|
28
|
+
STDERR.puts e.message.bold
|
29
|
+
retry if (retries += 1) < 3
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def clipboard_copy(key)
|
35
|
+
require 'clipboard'
|
36
|
+
Clipboard.copy(key)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'shhh'
|
5
|
+
require 'shhh/errors'
|
6
|
+
require_relative 'command'
|
7
|
+
module Shhh
|
8
|
+
module App
|
9
|
+
module Commands
|
10
|
+
class OpenEditor < Command
|
11
|
+
include Shhh
|
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 run
|
22
|
+
begin
|
23
|
+
self.tempfile = ::Tempfile.new(::Base64.urlsafe_encode64(opts[:file]))
|
24
|
+
decrypt_content(self.tempfile)
|
25
|
+
result = process launch_editor
|
26
|
+
ensure
|
27
|
+
self.tempfile.close if tempfile
|
28
|
+
self.tempfile.unlink rescue nil
|
29
|
+
end
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
def launch_editor
|
34
|
+
system("#{cli.editor} #{tempfile.path}")
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def decrypt_content(file)
|
40
|
+
file.open
|
41
|
+
file.write(content)
|
42
|
+
file.flush
|
43
|
+
end
|
44
|
+
|
45
|
+
def content
|
46
|
+
@content ||= decr(File.read(opts[:file]), key)
|
47
|
+
end
|
48
|
+
|
49
|
+
def timestamp
|
50
|
+
@timestamp ||= Time.now.to_a.select { |d| d.is_a?(Fixnum) }.map { |d| '%02d' % d }[0..-3].reverse.join
|
51
|
+
end
|
52
|
+
|
53
|
+
def process(code)
|
54
|
+
if code == true
|
55
|
+
content_edited = File.read(tempfile.path)
|
56
|
+
md5 = ::Base64.encode64(Digest::MD5.new.digest(content))
|
57
|
+
md5_edited = ::Base64.encode64(Digest::MD5.new.digest(content_edited))
|
58
|
+
return 'No changes have been made.' if md5 == md5_edited
|
59
|
+
|
60
|
+
FileUtils.cp opts[:file], "#{opts[:file]}.#{timestamp}" if opts[:backup]
|
61
|
+
|
62
|
+
diff = compute_diff
|
63
|
+
|
64
|
+
File.open(opts[:file], 'w') { |f| f.write(encr(content_edited, key)) }
|
65
|
+
|
66
|
+
out = ''
|
67
|
+
if opts[:verbose]
|
68
|
+
out << "Saved encrypted/compressed content to #{opts[:file].bold.blue}" +
|
69
|
+
" (#{File.size(opts[:file]) / 1024}Kb), unencrypted size #{content.length / 1024}Kb."
|
70
|
+
out << (opts[:backup] ? ",\nbacked up the last version to #{backup_file.bold.blue}." : '.')
|
71
|
+
end
|
72
|
+
out << "\n\nDiff:\n#{diff}"
|
73
|
+
out
|
74
|
+
else
|
75
|
+
raise Shhh::Errors::EditorExitedAbnormally.new("#{cli.editor} exited with #{$<}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Computes the diff between two unencrypted versions
|
80
|
+
def compute_diff
|
81
|
+
original_content_file = Tempfile.new(rand(1024).to_s)
|
82
|
+
original_content_file.open
|
83
|
+
original_content_file.write(content)
|
84
|
+
original_content_file.flush
|
85
|
+
diff = `diff #{original_content_file.path} #{tempfile.path}`
|
86
|
+
diff.gsub!(/> (.*\n)/m, '\1'.green)
|
87
|
+
diff.gsub!(/< (.*\n)/m, '\1'.red)
|
88
|
+
ensure
|
89
|
+
original_content_file.close
|
90
|
+
original_content_file.unlink
|
91
|
+
diff
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
require 'shhh/app/keychain'
|
3
|
+
module Shhh
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class PrintKey < Command
|
7
|
+
include Shhh
|
8
|
+
required_options [ :keychain, :keyfile ]
|
9
|
+
|
10
|
+
try_after :show_examples
|
11
|
+
|
12
|
+
def run
|
13
|
+
cli.key
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'colored2'
|
2
|
+
require_relative 'command'
|
3
|
+
module Shhh
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class ShowExamples < Command
|
7
|
+
required_options :examples
|
8
|
+
try_after :show_help
|
9
|
+
|
10
|
+
def run
|
11
|
+
output = []
|
12
|
+
|
13
|
+
output << example(comment: 'generate a new private key into an environment variable:',
|
14
|
+
command: 'export KEY=$(shhh -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: 'shhh -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: 'shhh -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: 'shhh -d -s $(cat file.enc) -k $KEY',
|
30
|
+
result: 'secret string'.green)
|
31
|
+
|
32
|
+
output << example(comment: 'encrypt shhh.yml and save it to shhh.enc:',
|
33
|
+
command: 'shhh -e -f shhh.yml -o shhh.enc -k $KEY')
|
34
|
+
|
35
|
+
output << example(comment: 'decrypt an encrypted file and print it to STDOUT:',
|
36
|
+
command: 'shhh -df shhh.enc -k $KEY')
|
37
|
+
|
38
|
+
output << example(comment: 'edit an encrypted file in $EDITOR, ask for key, create a backup',
|
39
|
+
command: 'shhh -tibf ecrets.enc',
|
40
|
+
result: '
|
41
|
+
Private Key: ••••••••••••••••••••••••••••••••••••••••••••
|
42
|
+
Saved encrypted content to shhh.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
|
+
output.flatten.compact.join("\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
def example(comment: nil, command: nil, echo: nil, result: nil)
|
54
|
+
out = []
|
55
|
+
out << "# #{comment}".white.dark.italic if comment
|
56
|
+
out << "#{command}" if command
|
57
|
+
out << "#{echo}" if echo
|
58
|
+
out << "#{result}" if result
|
59
|
+
out << '—'*80
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
module Shhh
|
3
|
+
module App
|
4
|
+
module Commands
|
5
|
+
class ShowHelp < Command
|
6
|
+
|
7
|
+
required_options :help, ->(opts) { opts.keys.all? { |k| !opts[k] } }
|
8
|
+
try_after :generate_key, :open_editor, :encrypt_decrypt
|
9
|
+
|
10
|
+
def run
|
11
|
+
opts.to_s
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'tsort'
|
3
|
+
require 'pp'
|
4
|
+
module Shhh
|
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.to_hash)
|
48
|
+
end
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'shhh/errors'
|
2
|
+
|
3
|
+
module Shhh
|
4
|
+
module App
|
5
|
+
module Input
|
6
|
+
class Handler
|
7
|
+
def self.ask
|
8
|
+
retries ||= 0
|
9
|
+
prompt('Password: ', :green)
|
10
|
+
rescue ::OpenSSL::Cipher::CipherError
|
11
|
+
STDERR.puts 'Invalid password. Please try again.'
|
12
|
+
retry if (retries += 1) < 3
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.prompt(message, color)
|
17
|
+
HighLine.new(STDIN, STDERR).ask(message.bold) { |q| q.echo = '•'.send(color) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.new_password
|
21
|
+
password = prompt('New Password : ', :blue)
|
22
|
+
password_confirm = prompt('Confirm Password : ', :blue)
|
23
|
+
|
24
|
+
raise Shhh::Errors::PasswordsDontMatch.new(
|
25
|
+
'The passwords you entered do not match.') if password != password_confirm
|
26
|
+
|
27
|
+
raise Shhh::Errors::PasswordTooShort.new(
|
28
|
+
'Minimum length is 7 characters.') if password.length < 7
|
29
|
+
|
30
|
+
password
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|