Module: DotenvVault

Includes:
Dotenv
Defined in:
lib/dotenv-vault.rb,
lib/dotenv-vault/rails.rb,
lib/dotenv-vault/version.rb

Defined Under Namespace

Classes: DecryptionFailed, InvalidDotenvKey, NotFoundDotenvEnvironment, NotFoundDotenvKey, NotFoundDotenvVault, Railtie

Constant Summary collapse

VERSION =
"0.10.1"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.instrumenterObject

Returns the value of attribute instrumenter.



17
18
19
# File 'lib/dotenv-vault.rb', line 17

def instrumenter
  @instrumenter
end

Class Method Details

.decrypt(ciphertext, key) ⇒ Object

Raises:



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/dotenv-vault.rb', line 180

def decrypt(ciphertext, key)
  key = key[-64..-1] # last 64 characters. allows for passing keys with preface like key_*****

  raise InvalidDotenvKey, "INVALID_DOTENV_KEY: Key part must be 64 characters long (or more)" unless key && key.bytesize == 64

  lockbox = Lockbox.new(key: key, encode: true)
  begin
    lockbox.decrypt(ciphertext)
  rescue Lockbox::Error
    raise DecryptionFailed, "DECRYPTION_FAILED: Please check your DOTENV_KEY"
  end
end

.dotenv_keyObject



162
163
164
165
166
# File 'lib/dotenv-vault.rb', line 162

def dotenv_key
  return ENV["DOTENV_KEY"] if present?(ENV["DOTENV_KEY"])

  ""
end

.dotenv_key_present?Boolean

Returns:

  • (Boolean)


158
159
160
# File 'lib/dotenv-vault.rb', line 158

def dotenv_key_present?
  present?(dotenv_key) && dotenv_vault_present?
end

.dotenv_vault_present?Boolean

Returns:

  • (Boolean)


168
169
170
# File 'lib/dotenv-vault.rb', line 168

def dotenv_vault_present?
  File.file?(vault_path)
end

.ignoring_nonexistent_filesObject



85
86
87
# File 'lib/dotenv-vault.rb', line 85

def ignoring_nonexistent_files
  Dotenv.ignoring_nonexistent_files
end

.instructions(parsed, split_dotenv_key) ⇒ Object

Raises:



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/dotenv-vault.rb', line 193

def instructions(parsed, split_dotenv_key)
  # Parse DOTENV_KEY. Format is a URI
  uri = URI.parse(split_dotenv_key) # dotenv://:[email protected]/vault/.env.vault?environment=production

  # Get decrypt key
  key = uri.password
  raise InvalidDotenvKey, "INVALID_DOTENV_KEY: Missing key part" unless present?(key)

  # Get environment
  params = Hash[URI::decode_www_form(uri.query.to_s)]
  environment = params["environment"]
  raise InvalidDotenvKey, "INVALID_DOTENV_KEY: Missing environment part" unless present?(environment)

  # Get ciphertext payload
  environment_key = "DOTENV_VAULT_#{environment.upcase}"
  ciphertext = parsed[environment_key] # DOTENV_VAULT_PRODUCTION
  raise NotFoundDotenvEnvironment, "NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate #{environment_key} in .env.vault" unless ciphertext

  {
    ciphertext: ciphertext,
    key: key
  }
end

.instrument(name, payload = {}, &block) ⇒ Object



77
78
79
# File 'lib/dotenv-vault.rb', line 77

def instrument(name, payload = {}, &block)
  Dotenv.instrument(name, payload = {}, &block)
end

.load(*filenames) ⇒ Object



26
27
28
29
30
31
32
# File 'lib/dotenv-vault.rb', line 26

def load(*filenames)
  if using_vault?
    load_vault(*filenames)
  else
    Dotenv.load(*filenames) # fallback
  end
end

.load!(*filenames) ⇒ Object

same as ‘load`, but raises Errno::ENOENT if any files don’t exist



35
36
37
38
39
40
41
# File 'lib/dotenv-vault.rb', line 35

def load!(*filenames)
  if using_vault?
    load_vault(*filenames)
  else
    Dotenv.load!(*filenames)
  end
end

.load_vault(*filenames) ⇒ Object

Vault: Load from .env.vault

Decrypts and loads to ENV



92
93
94
95
96
97
98
99
100
101
# File 'lib/dotenv-vault.rb', line 92

def load_vault(*filenames)
  DotenvVault.logger.info("[dotenv-vault] Loading env from encrypted .env.vault") if DotenvVault.logger

  parsed = parse_vault(*filenames)
  
  # Set ENV
  parsed.each { |k, v| ENV[k] ||= v }

  { parsed: parsed }
end

.loggerObject



19
20
21
# File 'lib/dotenv-vault.rb', line 19

def logger
  @logger ||= Logger.new(STDOUT)
end

.overload(*filenames) ⇒ Object

same as ‘load`, but will override existing values in `ENV`



44
45
46
47
48
49
50
# File 'lib/dotenv-vault.rb', line 44

def overload(*filenames)
  if using_vault?
    overload_vault(*filenames)
  else
    Dotenv.overload(*filenames)
  end
end

.overload!(*filenames) ⇒ Object

same as ‘overload`, but raises Errno:ENOENT if any files don’t exist



53
54
55
56
57
58
59
# File 'lib/dotenv-vault.rb', line 53

def overload!(*filenames)
  if using_vault?
    overload_vault(*filenames)
  else
    Dotenv.overload!(*filenames)
  end
end

.overload_vault(*filenames) ⇒ Object

Vault: Overload from .env.vault

Decrypts and overloads to ENV



106
107
108
109
110
111
112
113
114
115
# File 'lib/dotenv-vault.rb', line 106

def overload_vault(*filenames)
  DotenvVault.logger.info("[dotenv-vault] Overloading env from encrypted .env.vault") if DotenvVault.logger

  parsed = parse_vault(*filenames)
  
  # Set ENV
  parsed.each { |k, v| ENV[k] = v }

  { parsed: parsed }
end

.parse(*filenames) ⇒ Object

returns a hash of parsed key/value pairs but does not modify ENV



62
63
64
65
66
67
68
# File 'lib/dotenv-vault.rb', line 62

def parse(*filenames)
  if using_vault?
    parse_vault(*filenames)
  else
    Dotenv.parse(*filenames)
  end
end

.parse_vault(*filenames) ⇒ Object

Raises:



117
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
# File 'lib/dotenv-vault.rb', line 117

def parse_vault(*filenames)
  # DOTENV_KEY=development/key_1234
  #
  # Warn the developer unless present
  raise NotFoundDotenvKey, "NOT_FOUND_DOTENV_KEY: Cannot find ENV['DOTENV_KEY']" unless present?(dotenv_key)

  # Parse .env.vault
  parsed = Dotenv.parse(vault_path)

  # handle scenario for comma separated keys - for use with key rotation
  # example: DOTENV_KEY="dotenv://:[email protected]/vault/.env.vault?environment=prod,dotenv://:[email protected]/vault/.env.vault?environment=prod"
  keys = dotenv_key.split(',')

  decrypted = nil
  keys.each_with_index do |split_dotenv_key, index|
    begin
      # Get full key
      key = split_dotenv_key.strip

      # Get instructions for decrypt
      attrs = instructions(parsed, key)

      # Decrypt
      decrypted = decrypt(attrs[:ciphertext], attrs[:key])

      break
    rescue => error
      # last key
      raise error if index >= keys.length - 1
      # try next key
    end
  end

  # Parse decrypted .env string
  Dotenv::Parser.call(decrypted, true)
end

.present?(str) ⇒ Boolean

Returns:

  • (Boolean)


176
177
178
# File 'lib/dotenv-vault.rb', line 176

def present?(str)
  !(str.nil? || str.empty?)
end

.require_keys(*keys) ⇒ Object



81
82
83
# File 'lib/dotenv-vault.rb', line 81

def require_keys(*keys)
  Dotenv.require_keys(*keys)
end

.using_vault?Boolean

Returns:

  • (Boolean)


154
155
156
# File 'lib/dotenv-vault.rb', line 154

def using_vault?
  dotenv_key_present? && dotenv_vault_present?
end

.vault_pathObject



172
173
174
# File 'lib/dotenv-vault.rb', line 172

def vault_path
  ".env.vault"
end

.with(*filenames) ⇒ Object

Internal: Helper to expand list of filenames.

Returns a hash of all the loaded environment variables.



73
74
75
# File 'lib/dotenv-vault.rb', line 73

def with(*filenames)
  Dotenv.with(*filenames)
end