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