secrets-cipher-base64 1.2.1
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 +314 -0
- data/Rakefile +13 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/keychain +38 -0
- data/exe/secrets +8 -0
- data/lib/secrets.rb +73 -0
- data/lib/secrets/app.rb +42 -0
- data/lib/secrets/app/cli.rb +197 -0
- data/lib/secrets/app/commands.rb +27 -0
- data/lib/secrets/app/commands/command.rb +55 -0
- data/lib/secrets/app/commands/delete_keychain_key.rb +15 -0
- data/lib/secrets/app/commands/encrypt_decrypt.rb +22 -0
- data/lib/secrets/app/commands/generate_key.rb +41 -0
- data/lib/secrets/app/commands/open_editor.rb +90 -0
- data/lib/secrets/app/commands/show_examples.rb +63 -0
- data/lib/secrets/app/commands/show_help.rb +13 -0
- data/lib/secrets/app/commands/show_version.rb +13 -0
- data/lib/secrets/app/keychain.rb +136 -0
- data/lib/secrets/app/outputs/to_file.rb +27 -0
- data/lib/secrets/app/outputs/to_stdout.rb +11 -0
- data/lib/secrets/app/password_handler.rb +39 -0
- data/lib/secrets/cipher_handler.rb +45 -0
- data/lib/secrets/configuration.rb +23 -0
- data/lib/secrets/data.rb +23 -0
- data/lib/secrets/data/decoder.rb +24 -0
- data/lib/secrets/data/encoder.rb +24 -0
- data/lib/secrets/data/wrapper_struct.rb +43 -0
- data/lib/secrets/errors.rb +27 -0
- data/lib/secrets/extensions/class_methods.rb +12 -0
- data/lib/secrets/extensions/instance_methods.rb +110 -0
- data/lib/secrets/version.rb +3 -0
- data/secrets-cipher-base64.gemspec +33 -0
- metadata +243 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
require 'secrets/app/keychain'
|
3
|
+
module Secrets
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class DeleteKeychainKey < Command
|
7
|
+
include Secrets
|
8
|
+
required_options :keychain_del
|
9
|
+
def run
|
10
|
+
Secrets::App::KeyChain.new(opts[:keychain_del]).delete
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
module Secrets
|
3
|
+
module App
|
4
|
+
module Commands
|
5
|
+
class EncryptDecrypt < Command
|
6
|
+
include Secrets
|
7
|
+
required_options :private_key,
|
8
|
+
[ :encrypt, :decrypt ],
|
9
|
+
[ :file, :string ]
|
10
|
+
def run
|
11
|
+
send(cli.action, content, opts[:private_key])
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def content
|
17
|
+
@content ||= (opts[:string] || (opts[:file].eql?('-') ? STDIN.read : File.read(opts[:file])))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'command'
|
2
|
+
require 'secrets/app/keychain'
|
3
|
+
module Secrets
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class GenerateKey < Command
|
7
|
+
include Secrets
|
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
|
+
handler = Secrets::App::PasswordHandler.new(opts).create
|
17
|
+
new_private_key = encr_password(new_private_key, handler.password)
|
18
|
+
end
|
19
|
+
|
20
|
+
clipboard_copy(new_private_key) if opts[:copy]
|
21
|
+
|
22
|
+
if opts[:keychain] && Secrets::App.is_osx?
|
23
|
+
Secrets::App::KeyChain.new(opts[:keychain]).add(new_private_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
new_private_key
|
27
|
+
rescue Secrets::Errors::PasswordsDontMatch, Secrets::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,90 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'secrets'
|
5
|
+
require 'secrets/errors'
|
6
|
+
require_relative 'command'
|
7
|
+
module Secrets
|
8
|
+
module App
|
9
|
+
module Commands
|
10
|
+
class OpenEditor < Command
|
11
|
+
include Secrets
|
12
|
+
required_options :private_key, :edit, :file
|
13
|
+
attr_accessor :tempfile
|
14
|
+
|
15
|
+
def run
|
16
|
+
begin
|
17
|
+
self.tempfile = ::Tempfile.new(::Base64.urlsafe_encode64(opts[:file]))
|
18
|
+
decrypt_content(self.tempfile)
|
19
|
+
result = process launch_editor
|
20
|
+
ensure
|
21
|
+
self.tempfile.close if tempfile
|
22
|
+
self.tempfile.unlink rescue nil
|
23
|
+
end
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
def launch_editor
|
28
|
+
system("#{cli.editor} #{tempfile.path}")
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def decrypt_content(file)
|
34
|
+
file.open
|
35
|
+
file.write(content)
|
36
|
+
file.flush
|
37
|
+
end
|
38
|
+
|
39
|
+
def content
|
40
|
+
@content ||= decr(File.read(opts[:file]), key)
|
41
|
+
end
|
42
|
+
|
43
|
+
def timestamp
|
44
|
+
@timestamp ||= Time.now.to_a.select { |d| d.is_a?(Fixnum) }.map { |d| '%02d' % d }[0..-3].reverse.join
|
45
|
+
end
|
46
|
+
|
47
|
+
def process(code)
|
48
|
+
if code == true
|
49
|
+
content_edited = File.read(tempfile.path)
|
50
|
+
md5 = ::Base64.encode64(Digest::MD5.new.digest(content))
|
51
|
+
md5_edited = ::Base64.encode64(Digest::MD5.new.digest(content_edited))
|
52
|
+
return 'No changes have been made.' if md5 == md5_edited
|
53
|
+
|
54
|
+
FileUtils.cp opts[:file], "#{opts[:file]}.#{timestamp}" if opts[:backup]
|
55
|
+
|
56
|
+
diff = compute_diff
|
57
|
+
|
58
|
+
File.open(opts[:file], 'w') { |f| f.write(encr(content_edited, key)) }
|
59
|
+
|
60
|
+
out = ''
|
61
|
+
if opts[:verbose]
|
62
|
+
out << "Saved encrypted/compressed content to #{opts[:file].bold.blue}" +
|
63
|
+
" (#{File.size(opts[:file]) / 1024}Kb), unencrypted size #{content.length / 1024}Kb."
|
64
|
+
out << (opts[:backup] ? ",\nbacked up the last version to #{backup_file.bold.blue}." : '.')
|
65
|
+
end
|
66
|
+
out << "\n\nDiff:\n#{diff}"
|
67
|
+
out
|
68
|
+
else
|
69
|
+
raise Secrets::Errors::EditorExitedAbnormally.new("#{cli.editor} exited with #{$<}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Computes the diff between two unencrypted versions
|
74
|
+
def compute_diff
|
75
|
+
original_content_file = Tempfile.new(rand(1024).to_s)
|
76
|
+
original_content_file.open
|
77
|
+
original_content_file.write(content)
|
78
|
+
original_content_file.flush
|
79
|
+
diff = `diff #{original_content_file.path} #{tempfile.path}`
|
80
|
+
diff.gsub!(/> (.*\n)/m, '\1'.green)
|
81
|
+
diff.gsub!(/< (.*\n)/m, '\1'.red)
|
82
|
+
ensure
|
83
|
+
original_content_file.close
|
84
|
+
original_content_file.unlink
|
85
|
+
diff
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'colored2'
|
2
|
+
require_relative 'command'
|
3
|
+
module Secrets
|
4
|
+
module App
|
5
|
+
module Commands
|
6
|
+
class ShowExamples < Command
|
7
|
+
required_options :examples
|
8
|
+
|
9
|
+
def run
|
10
|
+
output = []
|
11
|
+
|
12
|
+
output << example(comment: 'generate a new private key into an environment variable:',
|
13
|
+
command: 'export KEY=$(secrets -g)',
|
14
|
+
echo: 'echo $KEY',
|
15
|
+
result: '75ngenJpB6zL47/8Wo7Ne6JN1pnOsqNEcIqblItpfg4='.green)
|
16
|
+
|
17
|
+
output << example(comment: 'generate a new password-protected key, copy to the clipboard & save to a file',
|
18
|
+
command: 'secrets -gpc -o ~/.key',
|
19
|
+
echo: 'New Password : ' + '••••••••••'.green,
|
20
|
+
result: 'Confirm Password : ' + '••••••••••'.green)
|
21
|
+
|
22
|
+
output << example(comment: 'encrypt a plain text string with a key, and save the output to a file',
|
23
|
+
command: 'secrets -e -s ' + '"secret string"'.bold.yellow + ' -k $KEY -o file.enc',
|
24
|
+
echo: 'cat file.enc',
|
25
|
+
result: 'Y09MNDUyczU1S0UvelgrLzV0RTYxZz09CkBDMEw4Q0R0TmpnTm9md1QwNUNy%T013PT0K'.green)
|
26
|
+
|
27
|
+
output << example(comment: 'decrypt a previously encrypted string:',
|
28
|
+
command: 'secrets -d -s $(cat file.enc) -k $KEY',
|
29
|
+
result: 'secret string'.green)
|
30
|
+
|
31
|
+
output << example(comment: 'encrypt secrets.yml and save it to secrets.enc:',
|
32
|
+
command: 'secrets -e -f secrets.yml -o secrets.enc -k $KEY')
|
33
|
+
|
34
|
+
output << example(comment: 'decrypt an encrypted file and print it to STDOUT:',
|
35
|
+
command: 'secrets -df secrets.enc -k $KEY')
|
36
|
+
|
37
|
+
output << example(comment: 'edit an encrypted file in $EDITOR, ask for key, create a backup',
|
38
|
+
command: 'secrets -tibf ecrets.enc',
|
39
|
+
result: '
|
40
|
+
Private Key: ••••••••••••••••••••••••••••••••••••••••••••
|
41
|
+
Saved encrypted content to secrets.enc.
|
42
|
+
|
43
|
+
Diff:
|
44
|
+
3c3
|
45
|
+
'.white.dark + '# (c) 2015 Konstantin Gredeskoul. All rights reserved.'.red.bold + '
|
46
|
+
---' + '
|
47
|
+
# (c) 2016 Konstantin Gredeskoul. All rights reserved.'.green.bold)
|
48
|
+
|
49
|
+
output.flatten.compact.join("\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
def example(comment: nil, command: nil, echo: nil, result: nil)
|
53
|
+
out = []
|
54
|
+
out << "# #{comment}".white.dark.italic if comment
|
55
|
+
out << "#{command}" if command
|
56
|
+
out << "#{echo}" if echo
|
57
|
+
out << "#{result}" if result
|
58
|
+
out << '—'*80
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'secrets'
|
2
|
+
require 'secrets/app'
|
3
|
+
require 'secrets/errors'
|
4
|
+
|
5
|
+
|
6
|
+
module Secrets
|
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
|
+
|
14
|
+
class << self
|
15
|
+
attr_accessor :user, :kind, :sub_section
|
16
|
+
|
17
|
+
def configure
|
18
|
+
yield self
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate!
|
22
|
+
raise ArgumentError.new(
|
23
|
+
'User is not defined. Either set $USER in environment, or directly on the class.') unless self.user
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
configure do
|
28
|
+
@kind = 'secrets-cipher-base64'
|
29
|
+
@user = ENV['USER']
|
30
|
+
@sub_section = 'generic-password'
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :key_name, :opts, :stderr_disabled
|
34
|
+
|
35
|
+
def initialize(key_name, opts = {})
|
36
|
+
self.key_name = key_name
|
37
|
+
self.opts = opts
|
38
|
+
self.class.validate!
|
39
|
+
end
|
40
|
+
|
41
|
+
def add(password)
|
42
|
+
execute command(:add, "-w '#{password}' ")
|
43
|
+
end
|
44
|
+
|
45
|
+
def find
|
46
|
+
execute command(:find, ' -g -w ')
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete
|
50
|
+
execute command(:delete)
|
51
|
+
end
|
52
|
+
|
53
|
+
def execute(command)
|
54
|
+
command += ' 2>/dev/null' if stderr_disabled
|
55
|
+
puts "> #{command.yellow.green}" if opts[:verbose]
|
56
|
+
output = `#{command}`
|
57
|
+
result = $?
|
58
|
+
raise Secrets::Errors::ExternalCommandError.new("Command error: #{result}, command: #{command}") unless result.success?
|
59
|
+
output.chomp
|
60
|
+
rescue Errno::ENOENT => e
|
61
|
+
raise Secrets::Errors::ExternalCommandError.new("Command error: #{e.message}, command: #{command}")
|
62
|
+
end
|
63
|
+
|
64
|
+
def stderr_off
|
65
|
+
self.stderr_disabled = true
|
66
|
+
end
|
67
|
+
|
68
|
+
def stderr_on
|
69
|
+
self.stderr_disabled = false
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def command(action, extras = nil)
|
75
|
+
out = base_command(action)
|
76
|
+
out << extras if extras
|
77
|
+
out = out.join
|
78
|
+
# Do not actually ever run these commands on non MacOSX
|
79
|
+
out = "echo #{out}" unless Secrets::App.is_osx?
|
80
|
+
out
|
81
|
+
end
|
82
|
+
|
83
|
+
def base_command(action)
|
84
|
+
[
|
85
|
+
"security #{action}-#{self.class.sub_section} ",
|
86
|
+
"-a '#{self.class.user}' ",
|
87
|
+
"-D '#{self.class.kind}' ",
|
88
|
+
"-s '#{self.key_name}' "
|
89
|
+
]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
#
|
97
|
+
# Usage: add-generic-password [-a account] [-s service] [-w password] [options...] [-A|-T appPath] [keychain]
|
98
|
+
# -a Specify account name (required)
|
99
|
+
# -c Specify item creator (optional four-character code)
|
100
|
+
# -C Specify item type (optional four-character code)
|
101
|
+
# -D Specify kind (default is "application password")
|
102
|
+
# -G Specify generic attribute (optional)
|
103
|
+
# -j Specify comment string (optional)
|
104
|
+
# -l Specify label (if omitted, service name is used as default label)
|
105
|
+
# -s Specify service name (required)
|
106
|
+
# -p Specify password to be added (legacy option, equivalent to -w)
|
107
|
+
# -w Specify password to be added
|
108
|
+
# -A Allow any application to access this item without warning (insecure, not recommended!)
|
109
|
+
# -T Specify an application which may access this item (multiple -T options are allowed)
|
110
|
+
# -U Update item if it already exists (if omitted, the item cannot already exist)
|
111
|
+
#
|
112
|
+
# Usage: find-generic-password [-a account] [-s service] [options...] [-g] [keychain...]
|
113
|
+
# -a Match "account" string
|
114
|
+
# -c Match "creator" (four-character code)
|
115
|
+
# -C Match "type" (four-character code)
|
116
|
+
# -D Match "kind" string
|
117
|
+
# -G Match "value" string (generic attribute)
|
118
|
+
# -j Match "comment" string
|
119
|
+
# -l Match "label" string
|
120
|
+
# -s Match "service" string
|
121
|
+
# -g Display the password for the item found
|
122
|
+
# -w Display only the password on stdout
|
123
|
+
# If no keychains are specified to search, the default search list is used.
|
124
|
+
# Find a generic password item.
|
125
|
+
#
|
126
|
+
# Usage: delete-generic-password [-a account] [-s service] [options...] [keychain...]
|
127
|
+
# -a Match "account" string
|
128
|
+
# -c Match "creator" (four-character code)
|
129
|
+
# -C Match "type" (four-character code)
|
130
|
+
# -D Match "kind" string
|
131
|
+
# -G Match "value" string (generic attribute)
|
132
|
+
# -j Match "comment" string
|
133
|
+
# -l Match "label" string
|
134
|
+
# -s Match "service" string
|
135
|
+
# If no keychains are specified to search, the default search list is used.
|
136
|
+
# Delete a generic password item.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Secrets
|
2
|
+
module App
|
3
|
+
module Outputs
|
4
|
+
class ToFile
|
5
|
+
attr_accessor :cli
|
6
|
+
|
7
|
+
def initialize(cli)
|
8
|
+
self.cli = cli
|
9
|
+
end
|
10
|
+
|
11
|
+
def opts
|
12
|
+
cli.opts
|
13
|
+
end
|
14
|
+
|
15
|
+
def output_proc
|
16
|
+
->(data) {
|
17
|
+
File.open(opts[:output], 'w') { |f| f.write(data) }
|
18
|
+
if opts[:verbose]
|
19
|
+
puts %Q\File #{opts[:file].bold.green} (#{File.size(opts[:file])/1024}Kb) has been #{action}ypted.\ + "\n" +
|
20
|
+
%Q\Encrypted version written to #{(opts[:output] || 'STDOUT').bold.green} (#{File.size(opts[:output]) / 1024}Kb)\
|
21
|
+
end
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'secrets/errors'
|
2
|
+
|
3
|
+
module Secrets
|
4
|
+
module App
|
5
|
+
class PasswordHandler
|
6
|
+
attr_accessor :opts, :password
|
7
|
+
|
8
|
+
def initialize(opts)
|
9
|
+
self.opts = opts
|
10
|
+
end
|
11
|
+
|
12
|
+
def ask
|
13
|
+
retries ||= 0
|
14
|
+
self.password = self.class.handle_user_input('Password: ', :green)
|
15
|
+
rescue ::OpenSSL::Cipher::CipherError
|
16
|
+
STDERR.puts 'Invalid password. Please try again.'
|
17
|
+
retry if (retries += 1) < 3
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.handle_user_input(message, color)
|
21
|
+
HighLine.new(STDIN, STDERR).ask(message.bold) { |q| q.echo = '•'.send(color) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def create
|
25
|
+
if opts[:password]
|
26
|
+
self.password = self.class.handle_user_input('New Password : ', :blue)
|
27
|
+
password_confirm = self.class.handle_user_input('Confirm Password : ', :blue)
|
28
|
+
|
29
|
+
raise Secrets::Errors::PasswordsDontMatch.new(
|
30
|
+
'The passwords you entered do not match.') if password != password_confirm
|
31
|
+
|
32
|
+
raise Secrets::Errors::PasswordTooShort.new(
|
33
|
+
'Minimum length is 7 characters.') if password.length < 7
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|