shhh 1.3.0

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