sym 0.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +25 -0
- data/.document +2 -0
- data/.gitignore +6 -2
- data/.rspec +1 -1
- data/.rubocop.yml +1156 -0
- data/.travis.yml +10 -2
- data/.yardopts +5 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/MANAGING-KEYS.md +67 -0
- data/README.md +444 -12
- data/Rakefile +10 -2
- data/bin/sym.bash-completion +24 -0
- data/exe/keychain +38 -0
- data/exe/sym +20 -0
- data/lib/sym.rb +110 -2
- data/lib/sym/app.rb +56 -0
- data/lib/sym/app/args.rb +42 -0
- data/lib/sym/app/cli.rb +192 -0
- data/lib/sym/app/commands.rb +56 -0
- data/lib/sym/app/commands/command.rb +77 -0
- data/lib/sym/app/commands/delete_keychain_item.rb +17 -0
- data/lib/sym/app/commands/encrypt_decrypt.rb +26 -0
- data/lib/sym/app/commands/generate_key.rb +37 -0
- data/lib/sym/app/commands/open_editor.rb +97 -0
- data/lib/sym/app/commands/print_key.rb +15 -0
- data/lib/sym/app/commands/show_examples.rb +76 -0
- data/lib/sym/app/commands/show_help.rb +16 -0
- data/lib/sym/app/commands/show_language_examples.rb +81 -0
- data/lib/sym/app/commands/show_version.rb +14 -0
- data/lib/sym/app/input/handler.rb +41 -0
- data/lib/sym/app/keychain.rb +135 -0
- data/lib/sym/app/nlp.rb +18 -0
- data/lib/sym/app/nlp/constants.rb +32 -0
- data/lib/sym/app/nlp/translator.rb +61 -0
- data/lib/sym/app/nlp/usage.rb +72 -0
- data/lib/sym/app/output.rb +15 -0
- data/lib/sym/app/output/base.rb +61 -0
- data/lib/sym/app/output/file.rb +18 -0
- data/lib/sym/app/output/noop.rb +14 -0
- data/lib/sym/app/output/stdout.rb +13 -0
- data/lib/sym/app/password/cache.rb +63 -0
- data/lib/sym/app/private_key/base64_decoder.rb +17 -0
- data/lib/sym/app/private_key/decryptor.rb +71 -0
- data/lib/sym/app/private_key/detector.rb +42 -0
- data/lib/sym/app/private_key/handler.rb +44 -0
- data/lib/sym/app/short_name.rb +10 -0
- data/lib/sym/application.rb +114 -0
- data/lib/sym/cipher_handler.rb +46 -0
- data/lib/sym/configuration.rb +39 -0
- data/lib/sym/data.rb +23 -0
- data/lib/sym/data/decoder.rb +28 -0
- data/lib/sym/data/encoder.rb +24 -0
- data/lib/sym/data/wrapper_struct.rb +43 -0
- data/lib/sym/encrypted_file.rb +34 -0
- data/lib/sym/errors.rb +37 -0
- data/lib/sym/extensions/class_methods.rb +12 -0
- data/lib/sym/extensions/instance_methods.rb +114 -0
- data/lib/sym/version.rb +1 -1
- data/sym.gemspec +34 -15
- metadata +224 -9
@@ -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.
|
data/lib/sym/app/nlp.rb
ADDED
@@ -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
|