secrets-cipher-base64 1.2.1
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 +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
|