secrets-cipher-base64 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +25 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +13 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE +22 -0
  9. data/MANAGING-KEYS.md +67 -0
  10. data/README.md +314 -0
  11. data/Rakefile +13 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/exe/keychain +38 -0
  15. data/exe/secrets +8 -0
  16. data/lib/secrets.rb +73 -0
  17. data/lib/secrets/app.rb +42 -0
  18. data/lib/secrets/app/cli.rb +197 -0
  19. data/lib/secrets/app/commands.rb +27 -0
  20. data/lib/secrets/app/commands/command.rb +55 -0
  21. data/lib/secrets/app/commands/delete_keychain_key.rb +15 -0
  22. data/lib/secrets/app/commands/encrypt_decrypt.rb +22 -0
  23. data/lib/secrets/app/commands/generate_key.rb +41 -0
  24. data/lib/secrets/app/commands/open_editor.rb +90 -0
  25. data/lib/secrets/app/commands/show_examples.rb +63 -0
  26. data/lib/secrets/app/commands/show_help.rb +13 -0
  27. data/lib/secrets/app/commands/show_version.rb +13 -0
  28. data/lib/secrets/app/keychain.rb +136 -0
  29. data/lib/secrets/app/outputs/to_file.rb +27 -0
  30. data/lib/secrets/app/outputs/to_stdout.rb +11 -0
  31. data/lib/secrets/app/password_handler.rb +39 -0
  32. data/lib/secrets/cipher_handler.rb +45 -0
  33. data/lib/secrets/configuration.rb +23 -0
  34. data/lib/secrets/data.rb +23 -0
  35. data/lib/secrets/data/decoder.rb +24 -0
  36. data/lib/secrets/data/encoder.rb +24 -0
  37. data/lib/secrets/data/wrapper_struct.rb +43 -0
  38. data/lib/secrets/errors.rb +27 -0
  39. data/lib/secrets/extensions/class_methods.rb +12 -0
  40. data/lib/secrets/extensions/instance_methods.rb +110 -0
  41. data/lib/secrets/version.rb +3 -0
  42. data/secrets-cipher-base64.gemspec +33 -0
  43. 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,13 @@
1
+ require_relative 'command'
2
+ module Secrets
3
+ module App
4
+ module Commands
5
+ class ShowHelp < Command
6
+ required_options :help, ->(opts) { opts.keys.all? { |k| !opts[k] } }
7
+ def run
8
+ opts.to_s
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'command'
2
+ module Secrets
3
+ module App
4
+ module Commands
5
+ class ShowVersion < Command
6
+ required_options :version
7
+ def run
8
+ "secrets-cipher-base64 (version #{Secrets::VERSION})"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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,11 @@
1
+ module Secrets
2
+ module App
3
+ module Outputs
4
+ class ToStdout < ToFile
5
+ def output_proc
6
+ ->(argument) { puts argument }
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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