shhh 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) 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 +328 -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/shhh +8 -0
  16. data/lib/shhh/app/args.rb +17 -0
  17. data/lib/shhh/app/cli.rb +150 -0
  18. data/lib/shhh/app/commands/command.rb +68 -0
  19. data/lib/shhh/app/commands/delete_keychain_item.rb +17 -0
  20. data/lib/shhh/app/commands/encrypt_decrypt.rb +26 -0
  21. data/lib/shhh/app/commands/generate_key.rb +41 -0
  22. data/lib/shhh/app/commands/open_editor.rb +96 -0
  23. data/lib/shhh/app/commands/print_key.rb +18 -0
  24. data/lib/shhh/app/commands/show_examples.rb +64 -0
  25. data/lib/shhh/app/commands/show_help.rb +16 -0
  26. data/lib/shhh/app/commands/show_version.rb +14 -0
  27. data/lib/shhh/app/commands.rb +55 -0
  28. data/lib/shhh/app/input/handler.rb +35 -0
  29. data/lib/shhh/app/keychain.rb +135 -0
  30. data/lib/shhh/app/output/file.rb +23 -0
  31. data/lib/shhh/app/output/stdout.rb +11 -0
  32. data/lib/shhh/app/private_key/base64_decoder.rb +17 -0
  33. data/lib/shhh/app/private_key/decryptor.rb +50 -0
  34. data/lib/shhh/app/private_key/detector.rb +34 -0
  35. data/lib/shhh/app/private_key/handler.rb +34 -0
  36. data/lib/shhh/app.rb +45 -0
  37. data/lib/shhh/cipher_handler.rb +45 -0
  38. data/lib/shhh/configuration.rb +23 -0
  39. data/lib/shhh/data/decoder.rb +28 -0
  40. data/lib/shhh/data/encoder.rb +24 -0
  41. data/lib/shhh/data/wrapper_struct.rb +43 -0
  42. data/lib/shhh/data.rb +23 -0
  43. data/lib/shhh/errors.rb +27 -0
  44. data/lib/shhh/extensions/class_methods.rb +12 -0
  45. data/lib/shhh/extensions/instance_methods.rb +114 -0
  46. data/lib/shhh/version.rb +3 -0
  47. data/lib/shhh.rb +73 -0
  48. data/shhh.gemspec +33 -0
  49. metadata +249 -0
@@ -0,0 +1,135 @@
1
+ require 'shhh'
2
+ require 'shhh/app'
3
+ require 'shhh/errors'
4
+
5
+
6
+ module Shhh
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 = 'shhh'
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 Shhh::Errors::ExternalCommandError.new("Command error: #{result}, command: #{command}") unless result.success?
58
+ output.chomp
59
+ rescue Errno::ENOENT => e
60
+ raise Shhh::Errors::ExternalCommandError.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 #{Shhh::App.this_os}?\nAre you sure?" unless Shhh::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.
@@ -0,0 +1,23 @@
1
+ module Shhh
2
+ module App
3
+ module Output
4
+ class File
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
+ }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ module Shhh
2
+ module App
3
+ module Output
4
+ class Stdout < File
5
+ def output_proc
6
+ ->(argument) { puts argument }
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Shhh
2
+ module App
3
+ module PrivateKey
4
+ class Base64Decoder < Struct.new(:encoded_key)
5
+
6
+ def key
7
+ return nil if encoded_key.nil?
8
+ begin
9
+ Base64.urlsafe_decode64(encoded_key)
10
+ rescue ArgumentError
11
+ encoded_key
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,50 @@
1
+ require_relative 'decryptor'
2
+ module Shhh
3
+ module App
4
+ module PrivateKey
5
+ class Decryptor
6
+ include Shhh
7
+
8
+ attr_accessor :encrypted_key, :input_handler
9
+
10
+ def initialize(encrypted_key, input_handler = Shhh::App::Input::Handler)
11
+ self.encrypted_key = encrypted_key
12
+ self.input_handler = input_handler
13
+ end
14
+
15
+ def key
16
+ return nil if encrypted_key.nil?
17
+ decrypted_key = nil
18
+ if should_decrypt?
19
+ begin
20
+ retries ||= 0
21
+ decrypted_key = decrypt(password)
22
+ rescue ::OpenSSL::Cipher::CipherError => e
23
+ STDERR.puts 'Invalid password. Please try again.'
24
+ ((retries += 1) < 3) ? retry : raise(Shhh::Errors::InvalidPasswordPrivateKey.new(e))
25
+ end
26
+ else
27
+ decrypted_key = encrypted_key
28
+ end
29
+ decrypted_key
30
+ end
31
+
32
+ private
33
+
34
+ def should_decrypt?
35
+ encrypted_key && (encrypted_key.length > 32)
36
+ end
37
+
38
+
39
+ def decrypt(password)
40
+ decr_password(encrypted_key, password)
41
+ end
42
+
43
+ def password
44
+ input_handler.ask
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ module Shhh
2
+ module App
3
+ module PrivateKey
4
+ class Detector < Struct.new(:opts) # :nodoc:
5
+ @mapping = Hash.new
6
+ class << self
7
+ attr_reader :mapping
8
+
9
+ def register(argument, proc)
10
+ self.mapping[argument] = proc
11
+ end
12
+ end
13
+
14
+ def key
15
+ self.class.mapping.each_pair do |options_key, key_proc|
16
+ return key_proc.call(self.opts[options_key]) if self.opts[options_key]
17
+ end
18
+ nil
19
+ end
20
+ end
21
+
22
+ Detector.register :private_key, ->(key) { key }
23
+ Detector.register :interactive, -> { Input::Handler.prompt('Private Key: ', :magenta) }
24
+ Detector.register :keychain, ->(key_name) { KeyChain.new(key_name).find }
25
+ Detector.register :keyfile, ->(file) {
26
+ begin
27
+ ::File.read(file)
28
+ rescue Errno::ENOENT
29
+ raise Shhh::Errors::FileNotFound.new("Encryption key file #{file} was not found.")
30
+ end
31
+ }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'detector'
2
+ require_relative 'base64_decoder'
3
+ require_relative 'decryptor'
4
+ module Shhh
5
+ module App
6
+ module PrivateKey
7
+ # This class figures out what is the private key that is
8
+ # provided to be used.
9
+ class Handler
10
+ include Shhh
11
+
12
+ attr_accessor :opts, :key
13
+
14
+ def initialize(opts)
15
+ self.opts = opts
16
+
17
+
18
+ self.key =
19
+ begin
20
+ Detector.new(opts).key
21
+ rescue Shhh::Errors::Error => e
22
+ if Shhh::App::Args.new(opts).key? && key.nil?
23
+ raise e
24
+ end
25
+ end
26
+
27
+ if key && key.length > 45
28
+ self.key = Decryptor.new(Base64Decoder.new(key).key).key
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/shhh/app.rb ADDED
@@ -0,0 +1,45 @@
1
+ require 'shhh/data'
2
+ require 'active_support/inflector'
3
+ module Shhh
4
+ # The +App+ Module is responsible for handing user input and executing commands.
5
+ # Central class in this module is the +CLI+ class.
6
+
7
+ # This module is responsible for printing pretty errors and maintaining the
8
+ # future exit code class-global variable.
9
+
10
+ module App
11
+ class << self
12
+ attr_accessor :exit_code
13
+ end
14
+
15
+ self.exit_code = 0
16
+
17
+ def self.out
18
+ STDERR
19
+ end
20
+
21
+ def self.error(
22
+ config: {},
23
+ exception: nil,
24
+ type: nil,
25
+ details: nil,
26
+ reason: nil)
27
+
28
+ self.out.puts([\
29
+ "#{(type || exception.class.name).titleize}:".red.bold.underlined +
30
+ (sprintf ' %s', details || exception.message).red.italic,
31
+ reason ? "\n#{reason.blue.bold.italic}" : nil].compact.join("\n"))
32
+ self.out.puts "\n" + exception.backtrace.join("\n").bold.red if exception && config && config[:trace]
33
+ self.exit_code = 1
34
+ end
35
+
36
+ def self.is_osx?
37
+ Gem::Platform.local.os.eql?('darwin')
38
+ end
39
+ def self.this_os
40
+ Gem::Platform.local.os
41
+ end
42
+ end
43
+ end
44
+
45
+ Shhh.dir_r 'shhh/app'
@@ -0,0 +1,45 @@
1
+ require 'base64'
2
+ require_relative 'configuration'
3
+
4
+ module Shhh
5
+ #
6
+ # +CipherHandler+ contains cipher-related utilities necessary to create
7
+ # ciphers, and seed them with the salt or iV vector,
8
+ #
9
+ module CipherHandler
10
+
11
+ CREATE_CIPHER = ->(name) { ::OpenSSL::Cipher.new(name) }
12
+
13
+ CipherStruct = Struct.new(:cipher, :iv, :salt)
14
+
15
+ def create_cipher(direction:,
16
+ cipher_name:,
17
+ iv: nil,
18
+ salt: nil)
19
+
20
+ cipher = new_cipher(cipher_name)
21
+ cipher.send(direction)
22
+ iv ||= cipher.random_iv
23
+ cipher.iv = iv
24
+ CipherStruct.new(cipher, iv, salt)
25
+ end
26
+
27
+ def new_cipher(cipher_name)
28
+ CREATE_CIPHER.call(cipher_name)
29
+ end
30
+
31
+ def update_cipher(cipher, value)
32
+ data = cipher.update(value)
33
+ data << cipher.final
34
+ data
35
+ end
36
+
37
+ module ClassMethods
38
+ def create_private_key
39
+ key = CREATE_CIPHER.call(Shhh::Configuration.property(:private_key_cipher)).random_key
40
+ ::Base64.urlsafe_encode64(key)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
@@ -0,0 +1,23 @@
1
+ module Shhh
2
+ # Application configuration Singleton class.
3
+ # It's values are requested by the library upon encryption or
4
+ # decryption, or any other operation.
5
+
6
+ class Configuration
7
+ class << self
8
+ attr_accessor :config
9
+
10
+ def configure
11
+ self.config ||= Configuration.new
12
+ yield config if block_given?
13
+ end
14
+
15
+ def property(name)
16
+ self.config.send(name)
17
+ end
18
+ end
19
+
20
+ attr_accessor :data_cipher, :password_cipher, :private_key_cipher
21
+ attr_accessor :compression_enabled, :compression_level
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ require_relative '../errors'
2
+ require 'base64'
3
+ require 'zlib'
4
+ module Shhh
5
+ module Data
6
+ class Decoder
7
+ attr_accessor :data, :data_encoded, :data
8
+
9
+ def initialize(data_encoded, compress)
10
+ self.data_encoded = data_encoded
11
+ self.data = begin
12
+ Base64.urlsafe_decode64(data_encoded)
13
+ rescue
14
+ data_encoded
15
+ end
16
+
17
+ if compress.nil? || compress # auto-guess
18
+ self.data = begin
19
+ Zlib::Inflate.inflate(data)
20
+ rescue Zlib::Error => e
21
+ data
22
+ end
23
+ end
24
+ self.data = Marshal.load(data)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ require 'base64'
2
+ require 'zlib'
3
+
4
+ require 'shhh/errors'
5
+ require 'shhh/configuration'
6
+
7
+ module Shhh
8
+ module Data
9
+ class Encoder
10
+ attr_accessor :data, :data_encoded
11
+
12
+ def initialize(data, compress)
13
+ self.data = data
14
+ self.data_encoded = Marshal.dump(data)
15
+ self.data_encoded = Zlib::Deflate.deflate(data_encoded, compression_level) if compress
16
+ self.data_encoded = Base64.urlsafe_encode64(data_encoded)
17
+ end
18
+
19
+ def compression_level
20
+ Shhh::Configuration.config.compression_level
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ require_relative '../errors'
2
+ module Shhh
3
+ module Data
4
+ class WrapperStruct < Struct.new(
5
+ :encrypted_data, # [Blob] Binary encrypted data (possibly compressed)
6
+ :iv, # [String] IV used to encrypt the data
7
+ :cipher_name, # [String] Name of the cipher used
8
+ :salt, # [Integer] For password-encrypted data this is the salt
9
+ :version, # [Integer] Version of the cipher used
10
+ :compress # [Boolean] indicates if compression should be applied
11
+ )
12
+
13
+ VERSION = 1
14
+
15
+ attr_accessor :compressed
16
+
17
+ def initialize(
18
+ encrypted_data:, # [Blob] Binary encrypted data (possibly compressed)
19
+ iv:, # [String] IV used to encrypt the data
20
+ cipher_name:, # [String] Name of the cipher used
21
+ salt: nil, # [Integer] For password-encrypted data this is the salt
22
+ version: VERSION, # [Integer] Version of the cipher used
23
+ compress: Shhh::Configuration.config.compression_enabled
24
+ )
25
+ super(encrypted_data, iv, cipher_name, salt, version, compress)
26
+ end
27
+
28
+ def config
29
+ Shhh::Configuration.config
30
+ end
31
+
32
+ def serialize
33
+ Marshal.dump(self)
34
+ end
35
+
36
+ def self.deserialize(data)
37
+ Marshal.load(data)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+
data/lib/shhh/data.rb ADDED
@@ -0,0 +1,23 @@
1
+ require_relative 'errors'
2
+ require 'base64'
3
+ require 'zlib'
4
+
5
+ require_relative 'data/wrapper_struct'
6
+ require_relative 'data/encoder'
7
+ require_relative 'data/decoder'
8
+
9
+ module Shhh
10
+ # This module is responsible for taking arbitrary data of any format, and safely compressing
11
+ # the result of `Marshal.dump(data)` using Zlib, and then doing `#urlsafe_encode64` encoding
12
+ # to convert it to a string,
13
+ module Data
14
+ def encode(data, compress = true)
15
+ Encoder.new(data, compress).data_encoded
16
+ end
17
+
18
+ def decode(data_encoded, compress = nil)
19
+ Decoder.new(data_encoded, compress).data
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,27 @@
1
+ module Shhh
2
+ module Errors
3
+ # Exceptions superclass for this library.
4
+ class Shhh::Errors::Error < StandardError; end
5
+
6
+ # No secret has been provided for encryption or decryption
7
+ class NoPrivateKeyFound < Shhh::Errors::Error; end
8
+ class PasswordsDontMatch < Shhh::Errors::Error; end
9
+ class PasswordTooShort < Shhh::Errors::Error; end
10
+ class DataEncodingVersionMismatch< Shhh::Errors::Error; end
11
+ class EditorExitedAbnormally < Shhh::Errors::Error; end
12
+ class InvalidEncodingPrivateKey < Shhh::Errors::Error; end
13
+ class InvalidPasswordPrivateKey < Shhh::Errors::Error; end
14
+ class FileNotFound < Shhh::Errors::Error; end
15
+ class ExternalCommandError < Shhh::Errors::Error; end
16
+
17
+ # Method was called on an abstract class. Override such methods in
18
+ # subclasses, and use subclasses for instantiation of objects.
19
+ class AbstractMethodCalled < ArgumentError
20
+ def initialize(method, message = nil)
21
+ super("Abstract method call, on #{method}" + (message || ''))
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+
@@ -0,0 +1,12 @@
1
+ require 'base64'
2
+ require 'shhh/cipher_handler'
3
+
4
+ module Shhh
5
+ module Extensions
6
+ module ClassMethods
7
+ def self.extended(klass)
8
+ klass.extend Shhh::CipherHandler::ClassMethods
9
+ end
10
+ end
11
+ end
12
+ end