shhh 1.3.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.
- 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
|