class Arcanus::Chest
Encapsulates the collection of encrypted secrets managed by Arcanus
.
Constants
- ENCRYPTION_CIPHER
- SIGNATURE_SIZE_BITS
Public Class Methods
new(key:, chest_file_path:)
click to toggle source
# File lib/arcanus/chest.rb, line 12 def initialize(key:, chest_file_path:) @key = key @chest_file_path = chest_file_path @original_encrypted_hash = YAML.load_file(chest_file_path).to_hash @original_decrypted_hash = decrypt_hash(@original_encrypted_hash) @hash = Utils.deep_dup(@original_decrypted_hash) end
Public Instance Methods
[](key)
click to toggle source
Access the chest as if it were a hash.
@param key [String] @return [Object]
# File lib/arcanus/chest.rb, line 24 def [](key) if @hash.key?(key) value = @hash[key] if value.is_a?(Hash) Item.new(value, [key]) else value end else raise KeyError, "Key '#{key}' does not exist in this Arcanus chest", caller end end
fetch(*args)
click to toggle source
Fetch key from the chest as if it were a hash.
# File lib/arcanus/chest.rb, line 57 def fetch(*args) @hash.fetch(*args) end
get(key_path)
click to toggle source
Get value at the specified key path.
@param key_path [String] @return [Object]
# File lib/arcanus/chest.rb, line 88 def get(key_path) keys = key_path.split('.') keys.inject(@hash) { |hash, key| hash[key] } rescue NoMethodError raise Arcanus::Errors::InvalidKeyPathError, "Key path '#{key_path}' does not correspond to an actual key" end
method_missing(method_sym, *args)
click to toggle source
Calls superclass method
# File lib/arcanus/chest.rb, line 39 def method_missing(method_sym, *args) method_name = method_sym.to_s if @hash.key?(method_name) self[method_name] else super end end
respond_to?(method_sym, *)
click to toggle source
Calls superclass method
# File lib/arcanus/chest.rb, line 52 def respond_to?(method_sym, *) @hash.key?(method_sym.to_s) || super end
respond_to_missing?(method_name, *args)
click to toggle source
Calls superclass method
# File lib/arcanus/chest.rb, line 48 def respond_to_missing?(method_name, *args) @hash.key?(method_name.to_s) ? true : super end
save()
click to toggle source
For each key in the chest, encrypt the new value if it has changed.
The goal is to create a file where the only lines that differ are the keys that changed.
# File lib/arcanus/chest.rb, line 104 def save modified_hash = process_hash_changes(@original_encrypted_hash, @original_decrypted_hash, @hash) File.open(@chest_file_path, 'w') do |f| f.puts('# Do not edit this file directly! Run `arcanus edit`') f.write(modified_hash.to_yaml) end end
set(key_path, value)
click to toggle source
Set value for the specified key path.
@param key_path [String] @param value [Object]
# File lib/arcanus/chest.rb, line 75 def set(key_path, value) keys = key_path.split('.') nested_hash = keys[0..-2].inject(@hash) { |hash, key| hash[key] } nested_hash[keys[-1]] = value rescue NoMethodError raise Arcanus::Errors::InvalidKeyPathError, "Key path '#{key_path}' does not correspond to an actual key" end
to_hash()
click to toggle source
Returns contents of the chest as a hash.
# File lib/arcanus/chest.rb, line 62 def to_hash @hash.dup end
to_yaml()
click to toggle source
Returns contents of the chest as YAML.
# File lib/arcanus/chest.rb, line 67 def to_yaml @hash.to_yaml end
update(new_hash)
click to toggle source
# File lib/arcanus/chest.rb, line 96 def update(new_hash) @hash = new_hash end
Private Instance Methods
decrypt_hash(hash)
click to toggle source
# File lib/arcanus/chest.rb, line 148 def decrypt_hash(hash) hash.each_with_object({}) do |(key, value), decrypted_hash| begin decrypted_hash[key] = value.is_a?(Hash) ? decrypt_hash(value) : decrypt_value(value) rescue Errors::DecryptionError => ex raise Errors::DecryptionError, "Problem decrypting value for key '#{key}': #{ex.message}" end end end
decrypt_value(blob)
click to toggle source
# File lib/arcanus/chest.rb, line 179 def decrypt_value(blob) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength unless blob.is_a?(String) raise Errors::DecryptionError, "Expecting an encrypted blob but got '#{blob}'" end iv_b64, enc_sym_key_b64, encrypted_value_b64, salt, signature = blob.split(':') if signature.nil? || salt.nil? || encrypted_value_b64.nil? || enc_sym_key_b64.nil? || iv_b64.nil? raise Errors::DecryptionError, "Invalid blob format '#{blob}'. " \ 'Did you encrypt this with an older version of Arcanus?' end iv = Base64.decode64(iv_b64) sym_key = @key.decrypt(Base64.decode64(enc_sym_key_b64)) ENCRYPTION_CIPHER.reset.decrypt ENCRYPTION_CIPHER.iv = iv ENCRYPTION_CIPHER.key = sym_key dumped_value = ENCRYPTION_CIPHER.update(Base64.decode64(encrypted_value_b64)) + ENCRYPTION_CIPHER.final actual_signature = Digest::SHA2.new(SIGNATURE_SIZE_BITS).tap do |digest| digest << salt digest << dumped_value end.to_s if signature != actual_signature raise Errors::DecryptionError, 'Signature of decrypted value does not match: ' \ "expected #{signature} but got #{actual_signature}" end Marshal.load(dumped_value) # rubocop:disable MarshalLoad end
encrypt_value(value)
click to toggle source
# File lib/arcanus/chest.rb, line 159 def encrypt_value(value) dumped_value = Marshal.dump(value) # Create a random symmetric key so we can encrypt plaintext of arbitrary length sym_key = ENCRYPTION_CIPHER.reset.encrypt.random_key iv = Base64.encode64(ENCRYPTION_CIPHER.random_iv) enc_sym_key = Base64.encode64(@key.encrypt(sym_key)) encrypted_value = Base64.encode64(ENCRYPTION_CIPHER.update(dumped_value) + ENCRYPTION_CIPHER.final) salt = SecureRandom.hex(8) signature = Digest::SHA2.new(SIGNATURE_SIZE_BITS).tap do |digest| digest << salt digest << dumped_value end.to_s "#{iv}:#{enc_sym_key}:#{encrypted_value}:#{salt}:#{signature}" end
process_hash_changes(original_encrypted, original_decrypted, current)
click to toggle source
# File lib/arcanus/chest.rb, line 116 def process_hash_changes(original_encrypted, original_decrypted, current) # rubocop:disable Metrics/MethodLength, Metrics/LineLength result = {} current.keys.each do |key| value = current[key] result[key] = if original_encrypted.key?(key) # Key still exists; check if modified. if value.is_a?(Hash) if original_encrypted[key].is_a?(Hash) process_hash_changes(original_encrypted[key], original_decrypted[key], value) else # Key changed from single value to hash, so no previous has to compare against process_hash_changes({}, {}, value) end elsif original_decrypted[key] != value # Value was changed; encrypt the new value encrypt_value(value) else # Value wasn't changed; keep original encrypted blob original_encrypted[key] end else # Key was added value.is_a?(Hash) ? process_hash_changes({}, {}, value) : encrypt_value(value) end end result end