summaryrefslogtreecommitdiff
path: root/lib/chef/encrypted_data_bag_item.rb
blob: c36c6923c57514700b548c801030e43ec501f658 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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
153
154
155
156
157
158
159
160
161
#
# Author:: Seth Falcon (<seth@chef.io>)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative "config"
Chef.autoload :DataBagItem, File.expand_path("data_bag_item", __dir__)
require_relative "encrypted_data_bag_item/decryptor"
require_relative "encrypted_data_bag_item/encryptor"
require_relative "dist"

# An EncryptedDataBagItem represents a read-only data bag item where
# all values, except for the value associated with the id key, have
# been encrypted.
#
# EncryptedDataBagItem can be used in recipes to decrypt data bag item
# members.
#
# Data bag item values are assumed to have been encrypted using the
# default symmetric encryption provided by Encryptor.encrypt where
# values are converted to YAML prior to encryption.
#
# If the shared secret is not specified at initialization or load,
# then the contents of the file referred to in
# Chef::Config[:encrypted_data_bag_secret] will be used as the
# secret.  The default path is /etc/chef/encrypted_data_bag_secret
#
# EncryptedDataBagItem is intended to provide a means to avoid storing
# data bag items in the clear on the Chef server.  This provides some
# protection against a breach of the Chef server or of Chef server
# backup data.  Because the secret must be stored in the clear on any
# node needing access to an EncryptedDataBagItem, this approach
# provides no protection of data bag items from actors with access to
# such nodes in the infrastructure.
#
class Chef::EncryptedDataBagItem
  ALGORITHM = "aes-256-cbc".freeze
  AEAD_ALGORITHM = "aes-256-gcm".freeze

  #
  # === Synopsis
  #
  #   EncryptedDataBagItem.new(hash, secret)
  #
  # === Args
  #
  # +enc_hash+::
  #   The encrypted hash to be decrypted
  # +secret+::
  #   The raw secret key
  #
  # === Description
  #
  # Create a new encrypted data bag item for reading (decryption)
  #
  def initialize(enc_hash, secret)
    @enc_hash = enc_hash
    @secret = secret
  end

  def [](key)
    value = @enc_hash[key]
    if key == "id" || value.nil?
      value
    else
      Decryptor.for(value, @secret).for_decrypted_item
    end
  end

  def []=(key, value)
    raise ArgumentError, "assignment not supported for #{self.class}"
  end

  def to_h
    @enc_hash.keys.inject({}) { |hash, key| hash[key] = self[key]; hash }
  end

  alias_method :to_hash, :to_h

  def self.encrypt_data_bag_item(plain_hash, secret)
    plain_hash.inject({}) do |h, (key, val)|
      h[key] = if key != "id"
                 Encryptor.new(val, secret).for_encrypted_item
               else
                 val
               end
      h
    end
  end

  #
  # === Synopsis
  #
  #   EncryptedDataBagItem.load(data_bag, name, secret = nil)
  #
  # === Args
  #
  # +data_bag+::
  #   The name of the data bag to fetch
  # +name+::
  #   The name of the data bag item to fetch
  # +secret+::
  #   The raw secret key. If the +secret+ is nil, the value of the file at
  #   +Chef::Config[:encrypted_data_bag_secret]+ is loaded. See +load_secret+
  #   for more information.
  #
  # === Description
  #
  # Loads and decrypts the data bag item with the given name.
  #
  def self.load(data_bag, name, secret = nil)
    raw_hash = Chef::DataBagItem.load(data_bag, name)
    secret ||= load_secret
    new(raw_hash, secret)
  end

  def self.load_secret(path = nil)
    require "open-uri" unless defined?(OpenURI)
    path ||= Chef::Config[:encrypted_data_bag_secret]
    unless path
      raise ArgumentError, "No secret specified and no secret found at #{Chef::Config.platform_specific_path(Chef::Dist::CONF_DIR + "/encrypted_data_bag_secret")}"
    end

    secret = case path
             when %r{^\w+://}
               # We have a remote key
               begin
                 Kernel.open(path).read.strip
               rescue Errno::ECONNREFUSED
                 raise ArgumentError, "Remote key not available from '#{path}'"
               rescue OpenURI::HTTPError
                 raise ArgumentError, "Remote key not found at '#{path}'"
               end
             else
               unless File.exist?(path)
                 raise Errno::ENOENT, "file not found '#{path}'"
               end

               IO.read(path).strip
             end
    if secret.size < 1
      raise ArgumentError, "invalid zero length secret in '#{path}'"
    end

    secret
  end

end