Class: SecretKeys

Inherits:
Hash
  • Object
show all
Defined in:
lib/secret_keys.rb,
lib/secret_keys/version.rb

Overview

Load a JSON file with encrypted values. This value can be used as a hash.

Defined Under Namespace

Modules: CLI Classes: EncryptionKeyError, Encryptor, VersionError

Constant Summary collapse

VERSION =
File.read(File.join(__dir__, "..", "..", "VERSION")).chomp.freeze
CRYPTO_VERSION =
1

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path_or_stream, encryption_key = nil) ⇒ SecretKeys

Note:

If no encryption key is passed, this will defautl to env var SECRET_KEYS_ENCRYPTION_KEY

Parse a JSON or YAML stream or file with encrypted values. Any values in the ".encrypted" key in the document will be decrypted with the provided encryption key. If values were put into the ".encrypted" key manually and are not yet encrypted, they will be used as is without any decryption.

or (if that is empty) the value read from the file path in SECRET_KEYS_ENCRYPTION_KEY_FILE.

Parameters:

  • path_or_stream (String, #read, Hash) —

    path to a JSON/YAML file to load, an IO object, or a Hash (mostly for testing purposes)

  • encryption_key (String) (defaults to: nil) —

    secret to use for encryption/decryption



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/secret_keys.rb', line 57

def initialize(path_or_stream, encryption_key = nil)
  @encryption_key = nil
  @salt = nil
  @format = :json

  encryption_key = read_encryption_key(encryption_key)
  update_secret(key: encryption_key)
  path_or_stream = Pathname.new(path_or_stream) if path_or_stream.is_a?(String)
  load_secrets!(path_or_stream)

  super(@values)
end

Class Method Details

.atomic_write(path, content) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Write a file atomically by writing to a temporary file in the same directory and renaming it over the target path so that readers never see a partially written file. The mode of an existing file at the path will be preserved. New files are created readable and writable only by the owner.

Parameters:

  • path (String, Pathname) —

    path of the file to write

  • content (String) —

    contents of the file



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/secret_keys.rb', line 27

def self.atomic_write(path, content)
  path = File.expand_path(path.to_s)
  mode = begin
    File.stat(path).mode & 0o7777
  rescue Errno::ENOENT
    0o600
  end
  tmp_path = File.join(File.dirname(path), ".#{File.basename(path)}.tmp.#{Process.pid}.#{rand(1_000_000)}")
  begin
    File.open(tmp_path, "w") do |file| # rubocop:disable Style/FileWrite
      file.write(content)
    end
    File.chmod(mode, tmp_path)
    File.rename(tmp_path, path)
  ensure
    File.unlink(tmp_path) if File.exist?(tmp_path)
  end
  nil
end

Instance Method Details

#decrypt!(key) ⇒ void

This method returns an undefined value.

Mark the key as no longer being decrypted when the JSON is saved.

Parameters:

  • key (String) —

    key to mark as not needing encryption



91
92
93
94
# File 'lib/secret_keys.rb', line 91

def decrypt!(key)
  @secret_keys.delete(key)
  nil
end

#encrypt!(key) ⇒ void

This method returns an undefined value.

Mark the key as being encrypted when the JSON is saved.

Parameters:

  • key (String) —

    key to mark as needing encryption



82
83
84
85
# File 'lib/secret_keys.rb', line 82

def encrypt!(key)
  @secret_keys << key
  nil
end

#encrypted?(key) ⇒ Boolean

Return true if the key is encrypted.

Parameters:

  • key (String) —

    key to check

Returns:

  • (Boolean)


100
101
102
# File 'lib/secret_keys.rb', line 100

def encrypted?(key)
  @secret_keys.include?(key)
end

#encrypted_hash ⇒ Hash

Output the keys as a hash that matches the structure that can be loaded by the initalizer. Values that have not changed will not be re-salted so the encrypted values will remain the same.

Returns:

  • (Hash) —

    An encrypted hash that can be saved/parsed by a new instance of SecretKeys

Raises:



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/secret_keys.rb', line 136

def encrypted_hash
  raise EncryptionKeyError.new("Encryption key not specified") if @encryption_key.nil? || @encryption_key.empty?

  hash = {}
  encrypted = {}
  @values.each do |key, value|
    if @secret_keys.include?(key)
      encrypted[key] = value
    else
      hash[key] = value
    end
  end

  unless encryption_key_matches?(@original_encrypted[ENCRYPTION_KEY])
    @original_encrypted = {}
  end
  encrypted.merge!(encrypt_values(encrypted, @original_encrypted))
  encrypted[SALT] = @salt
  encrypted[ENCRYPTION_KEY] = (@original_encrypted[ENCRYPTION_KEY] || encrypted_known_value)
  encrypted[VERSION_KEY] = CRYPTO_VERSION

  hash[ENCRYPTED] = encrypted
  hash
end

#encryption_key=(new_encryption_key) ⇒ void

This method returns an undefined value.

Change the encryption key in the document. When saving later, this key will be used.

Parameters:

  • new_encryption_key (String) —

    encryption key to use for future #save calls

Raises:



165
166
167
168
169
# File 'lib/secret_keys.rb', line 165

def encryption_key=(new_encryption_key)
  raise EncryptionKeyError.new("Encryption key not specified") if new_encryption_key.nil? || new_encryption_key.empty?
  @original_encrypted = {}
  update_secret(key: new_encryption_key)
end

#input_format ⇒ String

Return the data format (:json or :yaml) for the original data. Defaults to :json.

Returns:

  • (String)


174
175
176
# File 'lib/secret_keys.rb', line 174

def input_format
  @format
end

#save(path, format: nil) ⇒ void

This method returns an undefined value.

Save the encrypted hash to a file at the specified path. Encrypted values in an existing file will not be updated if the values have not changed (since each call uses a different initialization vector). This can be helpful if you have your secrets in source control so that only changed keys will actually be changed in the file when it is updated.

Parameters:

  • path (String, Pathname) —

    path of the file to save. If the file exists, only changed values will be updated.

  • format (String, Symbol) (defaults to: nil) —

    output format (YAML or JSON) to use. This will default based on the extension on the file path or the format originally used



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/secret_keys.rb', line 112

def save(path, format: nil)
  # create a copy of the encrypted hash for working on
  encrypted = encrypted_hash

  if format.nil?
    if yaml_file?(path)
      format = :yaml
    elsif json_file?(path)
      format = :json
    end
  end
  format ||= @format
  format = format.to_s.downcase

  output = ((format == "yaml") ? YAML.dump(encrypted) : JSON.pretty_generate(encrypted))
  output << $/ unless output.end_with?($/) # ensure file ends with system dependent new line
  self.class.atomic_write(path, output)
  nil
end

#to_h ⇒ Hash Also known as: to_hash

Convert into an actual Hash object.

Returns:

  • (Hash)


73
74
75
# File 'lib/secret_keys.rb', line 73

def to_h
  @values
end