Module: Vidibus::Secure

Defined in:
lib/vidibus/secure.rb,
lib/vidibus/secure/mongoid.rb,
lib/vidibus/secure/version.rb,
lib/vidibus/secure/extensions/controller.rb

Defined Under Namespace

Modules: Extensions, Mongoid Classes: DecryptError, Error, InputError, KeyError

Constant Summary collapse

GCM_VERSION =
0x02
CBC_VERSION =
0x01
GCM_IV_LEN =
12
GCM_TAG_LEN =
16
VERSION =
'5.0.0'

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.key_resolverObject

Returns the value of attribute key_resolver.



16
17
18
# File 'lib/vidibus/secure.rb', line 16

def key_resolver
  @key_resolver
end

Class Method Details

.current_keyObject

Returns the key to use for Mongoid attr_encrypted. Resolver wins over ENV; raises if neither is set.

Raises:



20
21
22
23
24
25
26
27
# File 'lib/vidibus/secure.rb', line 20

def current_key
  resolved = key_resolver&.call
  return resolved if resolved
  return ENV['VIDIBUS_SECURE_KEY'] if ENV['VIDIBUS_SECURE_KEY']
  raise KeyError,
    'No encryption key — set Vidibus::Secure.key_resolver ' \
    'or ENV["VIDIBUS_SECURE_KEY"]'
end

.decrypt(data, key, options = {}) ⇒ Object

Decrypts given data with given key. Dispatches on the version byte (0x02 GCM, 0x01 legacy CBC) and falls back to bare CBC (v4 layout) when no version byte is present.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/vidibus/secure.rb', line 74

def decrypt(data, key, options = {})
  unless key
    raise KeyError.new(
      'Please provide a secret key to decrypt data with.'
    )
  end
  return data if data.nil? || data == ''
  options = settings[:crypt].merge(options)
  raw = decode(data, options)
  plaintext = dispatch_decrypt(raw, key)
  return data unless plaintext
  begin
    JSON.parse(plaintext)
  rescue JSON::ParserError
    plaintext
  end
end

.encrypt(data, key, options = {}) ⇒ Object

Encrypts given data with given key (AES-256-GCM).



59
60
61
62
63
64
65
66
67
68
69
# File 'lib/vidibus/secure.rb', line 59

def encrypt(data, key, options = {})
  unless key
    raise KeyError.new(
      'Please provide a secret key to encrypt data with.'
    )
  end
  options = settings[:crypt].merge(options)
  data = JSON.generate(data) unless data.is_a?(String)
  blob = gcm_encrypt(data, derive_key(key))
  encode(blob, options)
end

.legacy_decrypt(data, key, options = {}) ⇒ Object

Decrypts a v4 (CBC) ciphertext, forcing the legacy path. Use this only for the rare case where a v4 blob’s first byte happens to be 0x01 or 0x02 and would otherwise be misrouted. The blob may carry a 0x01 version byte preamble or be bare base64/hex.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/vidibus/secure.rb', line 97

def legacy_decrypt(data, key, options = {})
  unless key
    raise KeyError.new(
      'Please provide a secret key to decrypt data with.'
    )
  end
  return data if data.nil? || data == ''
  options = settings[:crypt].merge(options)
  raw = decode(data, options)
  return nil unless raw && raw != ''
  ct = raw.getbyte(0) == CBC_VERSION ?
    raw.byteslice(1, raw.bytesize - 1) : raw
  plaintext = cbc_decrypt(ct, key)
  begin
    JSON.parse(plaintext)
  rescue JSON::ParserError
    plaintext
  end
end

.random(options = {}) ⇒ Object

Returns a truly random string.



39
40
41
42
43
# File 'lib/vidibus/secure.rb', line 39

def random(options = {})
  options = settings[:random].merge(options)
  length = options[:length]
  SecureRandom.send(options[:encoding], length)[0, length]
end

.settingsObject

Define default settings for random, sign, and crypt.



30
31
32
33
34
35
36
# File 'lib/vidibus/secure.rb', line 30

def settings
  @settings ||= {
    random: { length: 50, encoding: :base64 },
    sign:   { algorithm: 'SHA256', encoding: :hex },
    crypt:  { algorithm: 'AES-256-GCM', encoding: :base64 }
  }
end

.sign(data, key, options = {}) ⇒ Object

Returns signature of given data with given key.



46
47
48
49
50
51
52
53
54
55
56
# File 'lib/vidibus/secure.rb', line 46

def sign(data, key, options = {})
  unless key
    raise KeyError.new(
      'Please provide a secret key to sign data with.'
    )
  end
  options = settings[:sign].merge(options)
  digest = OpenSSL::Digest.new(options[:algorithm])
  signature = OpenSSL::HMAC.digest(digest, key, data)
  encode(signature, options)
end

.sign_request(verb, path, params, key, signature_param = nil) ⇒ Object

Signs request.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/vidibus/secure.rb', line 118

def sign_request(verb, path, params, key, signature_param = nil)
  default_signature_param = :sign
  params_given = !!params
  if params_given && !params.is_a?(Hash)
    raise InputError.new('Given params is not a Hash.')
  end
  params = {} unless params_given
  signature_param ||=
    if params_given && params.keys.first.is_a?(String)
      default_signature_param.to_s
    else
      default_signature_param
    end

  uri = URI.parse(path)
  path_params = Rack::Utils.parse_nested_query(uri.query)
  uri.query = nil

  _verb = verb.to_s.downcase
  _params = params.merge(path_params).
    except(signature_param.to_s, signature_param.to_s.to_sym)

  signature_string = [
    _verb,
    uri.to_s.gsub(/\/+$/, ''),
    _params.any? ? params_identifier(_params) : ''
  ].join('|')

  signature = sign(signature_string, key)

  if %w[post put].include?(_verb) ||
      (params_given && path_params.empty?)
    params[signature_param] = signature
  else
    unless path.gsub!(/(#{signature_param}=)[^&]+/,
                      "\\1#{signature}")
      glue = path.match(/\?/) ? '&' : '?'
      path << "#{glue}#{signature_param}=#{signature}"
    end
  end
  [path, params]
end

.verify_request(verb, path, params, key, signature_param = nil) ⇒ Object

Verifies that given request is valid.



162
163
164
165
166
167
168
# File 'lib/vidibus/secure.rb', line 162

def verify_request(verb, path, params, key, signature_param = nil)
  params ||= {}
  _path = path.dup
  _params = params.dup
  sign_request(verb, _path, _params, key, signature_param)
  path == _path && params == _params
end