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