lib/chef/encrypted_attribute/api.rb
# encoding: UTF-8
#
# Author:: Xabier de Zuazo (<xabier@zuazo.org>)
# Copyright:: Copyright (c) 2014 Onddo Labs, SL.
# 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 'chef/encrypted_attribute/config'
require 'chef/encrypted_attribute/encrypted_mash'
require 'chef/config'
require 'chef/encrypted_attribute/local_node'
require 'chef/encrypted_attribute/remote_node'
require 'chef/encrypted_attribute/encrypted_mash/version0'
require 'chef/encrypted_attribute/encrypted_mash/version1'
require 'chef/encrypted_attribute/encrypted_mash/version2'
class Chef
class EncryptedAttribute
# Main EncryptedAttribute class methods API module.
#
# All these methods are available as static methods in the
# {Chef::EncryptedAttribute} class.
#
# These methods are intended to be used from Chef
# [Recipes](http://docs.chef.io/recipes.html) or
# [Resources](https://docs.chef.io/resource.html).
#
# The attributes created by these methods are encrypted **only for the local
# node** by default.
#
# The static `*_on_node` methods can be used, although they have not been
# designed for this purpose (have not been tested).
#
# This module uses the {Chef::EncryptedAttribute} instance methods
# internally.
#
# # Configuration
#
# All the methods read the default configuration from the
# `Chef::Config[:encrypted_attributes]` hash. Most of methods also support
# setting some configuration parameters as last argument. Both the global
# and the method argument configuration will be merged.
#
# If the configuration value to be merged is an array or a hash (for example
# `keys`), the method argument configuration value has preference over the
# global configuration. arrays and hashes are not merged.
#
# Both `Chef::Config[:encrypted_attributes]` and method's `config` parameter
# should be a hash which may have any of the following keys:
#
# * `:version` - `EncryptedMash` format version to use, by default `1` is
# used which is recommended. The version `2` uses [GCM]
# (http://en.wikipedia.org/wiki/Galois/Counter_Mode) and probably should
# be considered the most secure, but it is disabled by default because it
# has some more requirements: Ruby `>= 2` and OpenSSL `>= 1.0.1`.
# * `:partial_search` - Whether to use Chef Server partial search, enabled
# by default. It may not work in some old versions of Chef Server.
# * `:client_search` - Search query for clients allowed to read the
# encrypted attribute. Can be a simple string or an array of queries to be
# *OR*-ed.
# * `:node_search` - Search query for nodes allowed to read the encrypted
# attribute. Can be a simple string or an array of queries to be *OR*-ed.
# * `:search_max_rows` - Maximum nodes returned by the internal chef
# searches. This number should be above the maximum expected nodes in the
# Chef Server. Defaults to `1000` nodes.
# * `:users` - Array of user names to be allowed to read the encrypted
# attribute(s). `"*"` to allow access to all users. Keep in mind that only
# admin clients or admin users are allowed to read user public keys. It is
# **not recommended** to use this from cookbooks unless you know what you
# are doing.
# * `:keys` - raw RSA public keys to be allowed to read encrypted
# attributes(s), in PEM (string) format. Can be client public keys, user
# public keys or any other RSA public key.
#
# @see Config
#
# For example, to disable Partial Search globally:
#
# ```ruby
# Chef::Config[:encrypted_attributes][:partial_search] = false
#
# # ftp_pass = Chef::EncryptedAttribute.load(node['myapp']['ftp_password'])
# # ...
# ```
#
# To disable Partial Search locally:
#
# ```ruby
# ftp_pass = Chef::EncryptedAttribute.load(
# node['myapp']['ftp_password'], :partial_search => false
# )
# ```
#
# To use protocol version 2 globally, which uses [GCM]
# (http://en.wikipedia.org/wiki/Galois/Counter_Mode):
#
# ```ruby
# Chef::Config[:encrypted_attributes][:version] = 2
# # ...
# ```
#
# If you want to use knife to work with encrypted attributes, surely you
# will need to save your Chef User public keys in a Data Bag (there is no
# need to encrypt them because they are public) and add them to the `:keys`
# configuration option. See the [Example Using User Keys Data Bag]
# (README.md#example-using-user-keys-data-bag) in the README for more
# information on this.
#
# # Caches
#
# This API uses some LRU caches to avoid making many requests to the Chef
# Server. All the caches are global and has the following methods:
#
# * `max_size` - Gets or sets the cache maximum item size.
# * `clear` - To empty the cache.
# * `[]` - To read a cache value (used internally).
# * `[]=` - To set a cache value (used internally).
#
# @see CacheLru
#
# This are the currently available caches:
#
# * `Chef::EncryptedAttribute::RemoteClients.cache` - Caches the
# `:client_search` query results (max_size: `1024`).
# * `Chef::EncryptedAttribute::RemoteNodes.cache` - Caches the
# `:node_search` query results (max_size: `1024`).
# * `Chef::EncryptedAttribute::RemoteUsers.cache` - Caches the Chef Users
# public keys (max_size: `1024`).
# * `Chef::EncryptedAttribute::RemoteNode.cache` - Caches the node
# (encrypted) attributes. Disabled by default (max_size: `0`).
#
# ### Clear All the Caches
#
# You can clear all the caches with the following code:
#
# ```ruby
# Chef::EncryptedAttribute::RemoteClients.cache.clear
# Chef::EncryptedAttribute::RemoteNodes.cache.clear
# Chef::EncryptedAttribute::RemoteUsers.cache.clear
# Chef::EncryptedAttribute::RemoteNode.cache.clear
# ```
#
# ### Disable All the Caches
#
# You can disable all the caches with the following code:
#
# ```ruby
# Chef::EncryptedAttribute::RemoteClients.cache.max_size(0)
# Chef::EncryptedAttribute::RemoteNodes.cache.max_size(0)
# Chef::EncryptedAttribute::RemoteUsers.cache.max_size(0)
# Chef::EncryptedAttribute::RemoteNode.cache.max_size(0)
# ```
#
# @see RemoteClients.cache
# @see RemoteNodes.cache
# @see RemoteUsers.cache
# @see RemoteNode.cache
module API
# Prints a Chef debug message.
#
# @param msg [String] message to print.
# @return void
# @api private
def debug(msg)
Chef::Log.debug("Chef::EncryptedAttribute: #{msg}")
end
# Prints a Chef warning message.
#
# @param msg [String] message to print.
# @return void
# @api private
def warn(msg)
Chef::Log.warn(msg)
end
# Gets local node object.
#
# @return [LocalNode] local node object.
# @api private
def local_node
LocalNode.new
end
# Creates a new {Config} object.
#
# Reads the default configuration from
# `Chef::Config[:encrypted_attributes]`.
#
# When the parameter is a {Chef::EncryptedAttribute::Config} class, all
# the configuration options will be replaced.
#
# When the parameter is a _Hash_, only the provided keys will be replaced.
#
# The local node public key will always be added to the provided
# configuration keys.
#
# @param arg [Config, Hash] the configuration to set.
# @return [Config] the read or set configuration object.
# @api private
def config(arg)
config =
EncryptedAttribute::Config.new(Chef::Config[:encrypted_attributes])
config.update!(arg)
config.keys(config.keys + [local_node.public_key])
config
end
# Reads an encrypted attribute from a hash, usually a node attribute.
#
# Uses the local private key to decrypt the attribute.
#
# An exception is thrown if the attribute cannot be decrypted or no
# encrypted attribute is found.
#
# @param enc_hs [Mash] an encrypted hash, usually a node attribute. For
# example: `node['myapp']['ftp_password']`.
# @param c [Config, Hash] a configuration hash. For example:
# `{ :partial_search => false }`.
# @return [Hash, Array, String, ...] the attribute in clear text,
# decrypted.
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
# format is wrong.
# @raise [UnsupportedEncryptedAttributeFormat] if encrypted attribute
# format is not supported or unknown.
def load(enc_hs, c = {})
debug("Loading Local Encrypted Attribute from: #{enc_hs.inspect}")
enc_attr = EncryptedAttribute.new(config(c))
result = enc_attr.load(enc_hs)
debug('Local Encrypted Attribute loaded.')
result
end
# Reads an encrypted attribute from a remote node.
#
# Uses the local private key to decrypt the attribute.
#
# An exception is thrown if the attribute cannot be decrypted or no
# encrypted attribute is found.
#
# @param name [String] the node name.
# @param attr_ary [Array<String>] the attribute path as *array of
# strings*. For example: `%w(myapp ftp_password)`.
# @param c [Config, Hash] a configuration hash. For example:
# `{ :partial_search => false }`.
# @return [Hash, Array, String, ...] decrypted attribute value.
# @raise [ArgumentError] if the attribute path format is wrong.
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
# format is wrong.
# @raise [UnsupportedEncryptedAttributeFormat] if encrypted attribute
# format is not supported or unknown.
# @raise [SearchFailure] if there is a Chef search error.
# @raise [SearchFatalError] if the Chef search response is wrong.
# @raise [InvalidSearchKeys] if search keys structure is wrong.
def load_from_node(name, attr_ary, c = {})
debug(
"Loading Remote Encrypted Attribute from #{name}: #{attr_ary.inspect}"
)
enc_attr = EncryptedAttribute.new(config(c))
result = enc_attr.load_from_node(name, attr_ary)
debug('Remote Encrypted Attribute loaded.')
result
end
# Creates an encrypted attribute.
#
# The returned value should be saved in a node attribute, like
# `node.normal[...] = Chef::EncryptedAttribute.create(...)`.
#
# The local node will always be able to decrypt the attribute.
#
# An exception is thrown if any error arises in the encryption process.
#
# @param value [Hash, Array, String, ...] the value to be encrypted. Can
# be a boolean, a number, a string, an array or a hash (the value will
# be converted to JSON internally).
# @param c [Config, Hash] a configuration hash. For example:
# `{ :client_search => "admin:true" }`.
# @return [EncryptedMash] encrypted attribute value. This is usually what
# is saved in the node attributes.
# @raise [ArgumentError] if user list is wrong.
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
# format is wrong or does not exist.
# @raise [UnsupportedEncryptedAttributeFormat] if encrypted attribute
# format is not supported or unknown.
# @raise [EncryptionFailure] if there are encryption errors.
# @raise [MessageAuthenticationFailure] if HMAC calculation error.
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
# @raise [InvalidKey] if the RSA key format is wrong.
# @raise [InsufficientPrivileges] if you lack enough privileges to read
# the keys from the Chef Server.
# @raise [ClientNotFound] if client does not exist.
# @raise [Net::HTTPServerException] for Chef Server HTTP errors.
# @raise [RequirementsFailure] if the specified encrypted attribute
# version cannot be used.
# @raise [SearchFailure] if there is a Chef search error.
# @raise [SearchFatalError] if the Chef search response is wrong.
# @raise [InvalidSearchKeys] if search keys structure is wrong.
def create(value, c = {})
debug('Creating Encrypted Attribute.')
enc_attr = EncryptedAttribute.new(config(c))
result = enc_attr.create(value)
debug('Encrypted Attribute created.')
result
end
# Creates an encrypted attribute on a remote node.
#
# Both the local node and the remote node will be able to decrypt the
# attribute.
#
# This method **requires admin privileges**. So in most cases, cannot be
# used from cookbooks.
#
# An exception is thrown if any error arises in the encryption process.
#
# @param name [String] the node name.
# @param attr_ary [Array<String>] the attribute path as *array of
# strings*. For example: `%w(myapp ftp_password)`.
# @param value [Hash, Array, String, Fixnum, ...] the value to be
# encrypted. Can be a boolean, a number, a string, an array or a hash
# (the value will be converted to JSON internally).
# @param c [Config, Hash] a configuration hash. For example:
# `{ :client_search => 'admin:true' }`.
# @return [EncryptedMash] encrypted attribute value.
# @raise [ArgumentError] if the attribute path format or the user list is
# wrong.
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
# format is wrong or does not exist.
# @raise [UnsupportedEncryptedAttributeFormat] if encrypted attribute
# format is not supported or unknown.
# @raise [EncryptionFailure] if there are encryption errors.
# @raise [MessageAuthenticationFailure] if HMAC calculation error.
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
# @raise [InvalidKey] if the RSA key format is wrong.
# @raise [InsufficientPrivileges] if you lack enough privileges to read
# the keys from the Chef Server.
# @raise [ClientNotFound] if client does not exist.
# @raise [Net::HTTPServerException] for Chef Server HTTP errors.
# @raise [RequirementsFailure] if the specified encrypted attribute
# version cannot be used.
# @raise [SearchFailure] if there is a Chef search error.
# @raise [SearchFatalError] if the Chef search response is wrong.
# @raise [InvalidSearchKeys] if search keys structure is wrong.
def create_on_node(name, attr_ary, value, c = {})
debug(
"Creating Remote Encrypted Attribute on #{name}: #{attr_ary.inspect}"
)
enc_attr = EncryptedAttribute.new(config(c))
result = enc_attr.create_on_node(name, attr_ary, value)
debug('Encrypted Remote Attribute created.')
result
end
# Updates who can read the attribute. This is intended to be used to
# update to the new nodes returned by `:client_search` and `:node_search`
# or perhaps global configuration changes.
#
# For example, in case new nodes are added or some are removed, and the
# clients returned by `:client_search` or `:node_search` are different,
# this `#update` method will decrypt the attribute and encrypt it again
# for the new nodes (or remove the old ones).
#
# If an update is made, the shared secrets are regenerated.
#
# Both the local node and the remote node will be able to decrypt the
# attribute.
#
# An exception is thrown if there is any error in the updating process.
#
# @param enc_hs [Mash] This must be a node encrypted attribute, this
# attribute will be updated, so it is mandatory to specify the type
# (usually `normal`). For example:
# `node.normal['myapp']['ftp_password']`.
# @param c [Config, Hash] a configuration hash. Surely you want this
# `#update` method to use the same `config` that the `#create` call.
# @return [Boolean] `true` if the encrypted attribute has been updated,
# `false` if not.
# @raise [ArgumentError] if user list is wrong.
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
# format is wrong or does not exist.
# @raise [UnsupportedEncryptedAttributeFormat] if encrypted attribute
# format is not supported or unknown.
# @raise [EncryptionFailure] if there are encryption errors.
# @raise [MessageAuthenticationFailure] if HMAC calculation error.
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
# @raise [InvalidKey] if the RSA key format is wrong.
# @raise [InsufficientPrivileges] if you lack enough privileges to read
# the keys from the Chef Server.
# @raise [ClientNotFound] if client does not exist.
# @raise [Net::HTTPServerException] for Chef Server HTTP errors.
# @raise [RequirementsFailure] if the specified encrypted attribute
# version cannot be used.
# @raise [SearchFailure] if there is a Chef search error.
# @raise [SearchFatalError] if the Chef search response is wrong.
# @raise [InvalidSearchKeys] if search keys structure is wrong.
def update(enc_hs, c = {})
debug("Updating Encrypted Attribute: #{enc_hs.inspect}")
enc_attr = EncryptedAttribute.new(config(c))
result = enc_attr.update(enc_hs)
if result
debug('Encrypted Attribute updated.')
else
debug('Encrypted Attribute not updated.')
end
result
end
# Updates who can decrypt the remote attribute.
#
# This method **requires admin privileges**. So in most cases, cannot be
# used from cookbooks.
#
# An exception is thrown if there is any error in the updating process.
#
# @param name [String] the node name.
# @param attr_ary [Array<String>] the attribute path as *array of
# strings*. For example: `%w(myapp ftp_password)`.
# @param c [Config, Hash] a configuration hash. Surely you want this
# `#update_on_node` method to use the same `config` that the `#create`
# call.
# @return [Boolean] `true` if the encrypted attribute has been updated,
# `false` if not.
# @raise [ArgumentError] if the attribute path format or the user list is
# wrong.
# @raise [UnacceptableEncryptedAttributeFormat] if encrypted attribute
# format is wrong or does not exist.
# @raise [UnsupportedEncryptedAttributeFormat] if encrypted attribute
# format is not supported or unknown.
# @raise [EncryptionFailure] if there are encryption errors.
# @raise [MessageAuthenticationFailure] if HMAC calculation error.
# @raise [InvalidPublicKey] if it is not a valid RSA public key.
# @raise [InvalidKey] if the RSA key format is wrong.
# @raise [InsufficientPrivileges] if you lack enough privileges to read
# the keys from the Chef Server.
# @raise [ClientNotFound] if client does not exist.
# @raise [Net::HTTPServerException] for Chef Server HTTP errors.
# @raise [RequirementsFailure] if the specified encrypted attribute
# version cannot be used.
# @raise [SearchFailure] if there is a Chef search error.
# @raise [SearchFatalError] if the Chef search response is wrong.
# @raise [InvalidSearchKeys] if search keys structure is wrong.
def update_on_node(name, attr_ary, c = {})
debug(
"Updating Remote Encrypted Attribute on #{name}: #{attr_ary.inspect}"
)
enc_attr = EncryptedAttribute.new(config(c))
result = enc_attr.update_on_node(name, attr_ary)
debug("Encrypted Remote Attribute #{result ? '' : 'not '}updated.")
result
end
# Checks whether an encrypted attribute exists.
#
# @param hs [Mash] an encrypted hash, usually a node attribute. The
# attribute type can be specified but is not necessary. For example:
# `node['myapp']['ftp_password']`.
# @return [Boolean] `true` if an encrypted attribute is found, `false` if
# not.
def exist?(hs)
debug("Checking if Encrypted Attribute exists here: #{hs.inspect}")
result = EncryptedMash.exist?(hs)
debug("Encrypted Attribute #{result ? '' : 'not '}found.")
result
end
# Checks whether an encrypted attribute exists in a remote node.
#
# @param [Mixed] args {#exist?} arguments.
# @return [Boolean] `true` if an encrypted attribute is found, `false` if
# not.
# @deprecated Use {#exist?} instead.
def exists?(*args)
warn("#{name}.exists? is deprecated in favor of #{name}.exist?.")
exist?(*args)
end
# Checks whether an encrypted attribute exists in a remote node.
#
# @param name [String] the node name.
# @param attr_ary [Array<String>] the attribute path as *array of
# strings*. For example: `%w(myapp ftp_password)`.
# @param c [Config, Hash] a configuration hash. For example:
# `{ :partial_search => false }`.
# @return [Boolean] `true` if an encrypted attribute is found, `false` if
# not.
# @raise [ArgumentError] if the attribute path format is wrong.
def exist_on_node?(name, attr_ary, c = {})
debug("Checking if Remote Encrypted Attribute exists on #{name}")
remote_node = RemoteNode.new(name)
config_merged = config(c)
node_attr =
remote_node.load_attribute(
attr_ary, config_merged.search_max_rows,
config_merged.partial_search
)
Chef::EncryptedAttribute.exist?(node_attr)
end
# Checks whether an encrypted attribute exists in a remote node.
#
# @param [Mixed] args {#exist_on_node?} arguments.
# @return [Boolean] `true` if an encrypted attribute is found, `false` if
# not.
# @deprecated Use {#exist_on_node?} instead.
def exists_on_node?(*args)
warn(
"#{name}.exists_on_node? is deprecated in favor of "\
"#{name}.exist_on_node?."
)
exist_on_node?(*args)
end
end
end
end