From 6a506d835f25257c9c2f6818b4cdf0786f1fd8aa Mon Sep 17 00:00:00 2001 From: Martin Popov Date: Mon, 19 Nov 2018 16:09:47 +0200 Subject: [PATCH 1/5] Fix typo in #memory_batch_encrypt --- lib/vault/rails.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vault/rails.rb b/lib/vault/rails.rb index f27b7377..550961f3 100644 --- a/lib/vault/rails.rb +++ b/lib/vault/rails.rb @@ -201,7 +201,7 @@ def memory_encrypt(path, key, plaintext, _client, convergent) # Perform in-memory encryption. This is useful for testing and development. def memory_batch_encrypt(path, key, plaintexts, _client) - plaintexts.map { |plaintext| memory_encrypt(path, key, ciphertext, _client, true) } + plaintexts.map { |plaintext| memory_encrypt(path, key, plaintext, _client, true) } end # Perform in-memory decryption. This is useful for testing and development. From 4cf08648ac21f3003edc04dc9ffb2612fd0794a1 Mon Sep 17 00:00:00 2001 From: Martin Popov Date: Mon, 19 Nov 2018 16:49:17 +0200 Subject: [PATCH 2/5] Add convergent field #passport_number for LazyPerson --- spec/dummy/app/models/lazy_person.rb | 2 ++ .../20181119142920_add_passport_number_to_people.rb | 5 +++++ spec/dummy/db/schema.rb | 7 ++++--- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 spec/dummy/db/migrate/20181119142920_add_passport_number_to_people.rb diff --git a/spec/dummy/app/models/lazy_person.rb b/spec/dummy/app/models/lazy_person.rb index 7478a6dc..31db6ca5 100644 --- a/spec/dummy/app/models/lazy_person.rb +++ b/spec/dummy/app/models/lazy_person.rb @@ -29,4 +29,6 @@ class LazyPerson < ActiveRecord::Base decode: ->(raw) { raw && raw[3...-3] } vault_attribute :non_ascii + + vault_attribute :passport_number, convergent: true end diff --git a/spec/dummy/db/migrate/20181119142920_add_passport_number_to_people.rb b/spec/dummy/db/migrate/20181119142920_add_passport_number_to_people.rb new file mode 100644 index 00000000..a3993bd2 --- /dev/null +++ b/spec/dummy/db/migrate/20181119142920_add_passport_number_to_people.rb @@ -0,0 +1,5 @@ +class AddPassportNumberToPeople < ActiveRecord::Migration[5.0] + def change + add_column :people, :passport_number_encrypted, :string + end +end diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index 78a737d5..316cfd02 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181030234312) do +ActiveRecord::Schema.define(version: 20181119142920) do create_table "people", force: :cascade do |t| t.string "name" @@ -20,8 +20,8 @@ t.string "business_card_encrypted" t.string "favorite_color_encrypted" t.string "non_ascii_encrypted" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "email_encrypted" t.string "county" t.string "county_encrypted" @@ -29,6 +29,7 @@ t.string "state_encrypted" t.string "date_of_birth" t.string "date_of_birth_encrypted" + t.string "passport_number_encrypted" end end From 67255d230e33581652dc20fa386d4db12180fb4f Mon Sep 17 00:00:00 2001 From: Martin Popov Date: Mon, 19 Nov 2018 10:41:03 +0200 Subject: [PATCH 3/5] Add EncryptedModel methods for batch operations .vault_persist_all and .vault_load_all are methods, that can be used for batch operations of several records. .vault_load_all is useful only when in combination with lazy decryption. --- lib/vault/encrypted_model.rb | 49 ++++++++++++++++++++++++++++++++ spec/integration/rails_spec.rb | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/lib/vault/encrypted_model.rb b/lib/vault/encrypted_model.rb index 576cf09b..cfe790c1 100644 --- a/lib/vault/encrypted_model.rb +++ b/lib/vault/encrypted_model.rb @@ -157,6 +157,55 @@ def vault_lazy_decrypt? def vault_lazy_decrypt! @vault_lazy_decrypt = true end + + # works only with convergent encryption + def vault_persist_all(attribute, records, plaintexts) + options = __vault_attributes[attribute] + + key = options[:key] + path = options[:path] + serializer = options[:serializer] + column = options[:encrypted_column] + + # Apply the serialize to the plaintext values, if one exists + if serializer + plaintexts = plaintexts.map { |plaintext| serializer.encode(plaintext) } + end + + # Generate the ciphertext and store it back as an attribute + ciphertexts = Vault::Rails.batch_encrypt(path, key, plaintexts, Vault.client) + + records.each_with_index do |record, index| + record.send("#{column}=", ciphertexts[index]) + record.save + end + end + + # works only with convergent encryption + # relevant only if lazy decryption is enabled + def vault_load_all(attribute, records) + options = __vault_attributes[attribute] + + key = options[:key] + path = options[:path] + serializer = options[:serializer] + column = options[:encrypted_column] + + # Load the ciphertext + ciphertexts = records.map { |record| record.read_attribute(column) } + + # Load the plaintext value + plaintexts = Vault::Rails.batch_decrypt(path, key, ciphertexts, Vault.client) + + # Deserialize the plaintext values, if a serializer exists + if serializer + plaintexts = plaintexts.map { |plaintext| serializer.decode(plaintext) } + end + + records.each_with_index do |record, index| + record.instance_variable_set("@#{attribute}", plaintexts[index]) + end + end end included do diff --git a/spec/integration/rails_spec.rb b/spec/integration/rails_spec.rb index 8d72ef64..24657455 100644 --- a/spec/integration/rails_spec.rb +++ b/spec/integration/rails_spec.rb @@ -481,4 +481,56 @@ end end end + + context 'batch encryption and decryption' do + before do + allow(Vault::Rails).to receive(:convergent_encryption_context).and_return('a' * 16).at_least(:once) + end + + describe '.vault_load_all' do + it 'calls Vault just once' do + first_person = LazyPerson.create!(passport_number: '12345678') + second_person = LazyPerson.create!(passport_number: '12345679') + + people = [first_person.reload, second_person.reload] + expect(Vault.logical).to receive(:write).once.and_call_original + LazyPerson.vault_load_all(:passport_number, people) + + first_person.passport_number + second_person.passport_number + end + + it 'loads the attribute of all records' do + first_person = LazyPerson.create!(passport_number: '12345678') + second_person = LazyPerson.create!(passport_number: '12345679') + + first_person.reload + second_person.reload + + LazyPerson.vault_load_all(:passport_number, [first_person, second_person]) + expect(first_person.passport_number).to eq('12345678') + expect(second_person.passport_number).to eq('12345679') + end + end + + describe '.vault_persist_all' do + it 'calls Vault just once' do + first_person = LazyPerson.new + second_person = LazyPerson.new + + expect(Vault.logical).to receive(:write).once.and_call_original + LazyPerson.vault_persist_all(:passport_number, [first_person, second_person], %w(12345678 12345679)) + end + + it 'saves the attribute of all records' do + first_person = LazyPerson.new + second_person = LazyPerson.new + + LazyPerson.vault_persist_all(:passport_number, [first_person, second_person], %w(12345678 12345679)) + + expect(first_person.reload.passport_number).to eq('12345678') + expect(second_person.reload.passport_number).to eq('12345679') + end + end + end end From f631432b5393e233b01bd7812b6ca52addb6c222 Mon Sep 17 00:00:00 2001 From: Martin Popov Date: Mon, 19 Nov 2018 18:14:20 +0200 Subject: [PATCH 4/5] Handle blank values in batch encryption and decryption --- lib/vault/rails.rb | 24 +++++++--- spec/unit/rails_spec.rb | 100 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/lib/vault/rails.rb b/lib/vault/rails.rb index 550961f3..a8df5b70 100644 --- a/lib/vault/rails.rb +++ b/lib/vault/rails.rb @@ -259,7 +259,9 @@ def vault_batch_encrypt(path, key, plaintexts, client) derived: true } - batch_input = plaintexts.map do |plaintext| + # Only present values can be encrypted by Vault. Empty values should be returned as they are. + non_empty_plaintexts = plaintexts.select { |plaintext| plaintext.present? } + batch_input = non_empty_plaintexts.map do |plaintext| { context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), plaintext: Base64.strict_encode64(plaintext) @@ -269,7 +271,11 @@ def vault_batch_encrypt(path, key, plaintexts, client) options.merge!(batch_input: batch_input) secret = client.logical.write(route, options) - secret.data[:batch_results].map { |result| result[:ciphertext] } + vault_results = secret.data[:batch_results].map { |result| result[:ciphertext] } + + plaintexts.map do |plaintext| + plaintext.present? ? vault_results.shift : plaintext + end end # Perform decryption using Vault. This will raise exceptions if Vault is @@ -296,18 +302,22 @@ def vault_batch_decrypt(path, key, ciphertexts, client) route = File.join(path, 'decrypt', key) - - batch_input = ciphertexts.map do |ciphertext| + # Only present values can be decrypted by Vault. Empty values should be returned as they are. + non_empty_ciphertexts = ciphertexts.select { |ciphertext| ciphertext.present? } + batch_input = non_empty_ciphertexts.map do |ciphertext| { context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), ciphertext: ciphertext } end - options = { batch_input: batch_input } secret = client.logical.write(route, options) - secret.data[:batch_results].map { |result| Base64.strict_decode64(result[:plaintext]) } + vault_results = secret.data[:batch_results].map { |result| Base64.strict_decode64(result[:plaintext]) } + + ciphertexts.map do |ciphertext| + ciphertext.present? ? vault_results.shift : ciphertext + end end # The symmetric key for the given params. @@ -320,7 +330,7 @@ def memory_key_for(path, key) # newly encoded string. # @return [String] def force_encoding(str) - str.force_encoding(Vault::Rails.encoding).encode(Vault::Rails.encoding) + str.blank? ? str : str.force_encoding(Vault::Rails.encoding).encode(Vault::Rails.encoding) end private diff --git a/spec/unit/rails_spec.rb b/spec/unit/rails_spec.rb index bc576315..aa860ea3 100644 --- a/spec/unit/rails_spec.rb +++ b/spec/unit/rails_spec.rb @@ -156,6 +156,56 @@ expect(Vault::Rails.batch_encrypt('path', 'key', ['plaintext1', 'plaintext2'], Vault::Rails.client)).to eq(%w(ciphertext1 ciphertext2)) end + + context 'with presented blank values' do + it 'sends the correct parameters to vault client' do + expected_route = 'path/encrypt/key' + expected_options = { + batch_input: [ + { + plaintext: Base64.strict_encode64('plaintext1'), + context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), + }, + { + plaintext: Base64.strict_encode64('plaintext2'), + context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), + }, + ], + convergent_encryption: true, + derived: true + } + + expect(Vault::Rails.client.logical).to receive(:write) + .with(expected_route, expected_options) + .and_return(spy('Vault::Secret')) + + Vault::Rails.batch_encrypt('path', 'key', ['plaintext1', '', 'plaintext2', '', nil, nil], Vault::Rails.client) + end + + it 'parses the response from vault client correctly and keeps the order of records' do + expected_route = 'path/encrypt/key' + expected_options = { + batch_input: [ + { + plaintext: Base64.strict_encode64('plaintext1'), + context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), + }, + { + plaintext: Base64.strict_encode64('plaintext2'), + context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), + }, + ], + convergent_encryption: true, + derived: true + } + + allow(Vault::Rails.client.logical).to receive(:write) + .with(expected_route, expected_options) + .and_return(instance_double('Vault::Secret', data: {:batch_results=>[{:ciphertext=>'ciphertext1'}, {:ciphertext=>'ciphertext2'}]})) + + expect(Vault::Rails.batch_encrypt('path', 'key', ['plaintext1', '', 'plaintext2', '', nil], Vault::Rails.client)).to eq(['ciphertext1', '', 'ciphertext2', '', nil]) + end + end end describe '.batch_decrypt' do @@ -207,5 +257,55 @@ expect(Vault::Rails.batch_decrypt('path', 'key', ['ciphertext1', 'ciphertext2'], Vault::Rails.client)).to eq( %w(plaintext1 plaintext2)) # in that order end + + context 'with presented blank values' do + it 'sends the correct parameters to vault client' do + expected_route = 'path/decrypt/key' + expected_options = { + batch_input: [ + { + ciphertext: 'ciphertext1', + context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), + }, + { + ciphertext: 'ciphertext2', + context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), + }, + ], + } + + expect(Vault::Rails.client.logical).to receive(:write) + .with(expected_route, expected_options) + .and_return(spy('Vault::Secret')) + + Vault::Rails.batch_decrypt('path', 'key', ['ciphertext1', '', 'ciphertext2', nil, '', ''], Vault::Rails.client) + end + + it 'parses the response from vault client correctly and keeps the order of records' do + expected_route = 'path/decrypt/key' + expected_options = { + batch_input: [ + { + ciphertext: 'ciphertext1', + context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), + }, + { + ciphertext: 'ciphertext2', + context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), + }, + { + ciphertext: 'ciphertext3', + context: Base64.strict_encode64(Vault::Rails.convergent_encryption_context), + }, + ], + } + + allow(Vault::Rails.client.logical).to receive(:write) + .with(expected_route, expected_options) + .and_return(instance_double('Vault::Secret', data: {batch_results: [{plaintext: 'cGxhaW50ZXh0MQ=='}, {plaintext:'cGxhaW50ZXh0Mg=='}, {plaintext: 'cGxhaW50ZXh0Mw=='}]})) + + expect(Vault::Rails.batch_decrypt('path', 'key', ['ciphertext1', '', nil, 'ciphertext2', '', 'ciphertext3'], Vault::Rails.client)).to eq( ['plaintext1', '', nil, 'plaintext2', '', 'plaintext3']) # in that order + end + end end end From cdbd1e295233463bbee312a93a0f5a009f873bdb Mon Sep 17 00:00:00 2001 From: Martin Popov Date: Tue, 20 Nov 2018 16:19:20 +0200 Subject: [PATCH 5/5] Extract attribute batch operations in PerformInBatches For both encryption and decryption we need the same initialization phase of reading the configuration options. The idea is to avoid code repetition and cluttering even more code in EncryptedModel. --- lib/vault/encrypted_model.rb | 38 +----- lib/vault/perform_in_batches.rb | 55 +++++++++ lib/vault/rails.rb | 1 + spec/unit/perform_in_batches_spec.rb | 168 +++++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 36 deletions(-) create mode 100644 lib/vault/perform_in_batches.rb create mode 100644 spec/unit/perform_in_batches_spec.rb diff --git a/lib/vault/encrypted_model.rb b/lib/vault/encrypted_model.rb index cfe790c1..a7b715f8 100644 --- a/lib/vault/encrypted_model.rb +++ b/lib/vault/encrypted_model.rb @@ -162,23 +162,7 @@ def vault_lazy_decrypt! def vault_persist_all(attribute, records, plaintexts) options = __vault_attributes[attribute] - key = options[:key] - path = options[:path] - serializer = options[:serializer] - column = options[:encrypted_column] - - # Apply the serialize to the plaintext values, if one exists - if serializer - plaintexts = plaintexts.map { |plaintext| serializer.encode(plaintext) } - end - - # Generate the ciphertext and store it back as an attribute - ciphertexts = Vault::Rails.batch_encrypt(path, key, plaintexts, Vault.client) - - records.each_with_index do |record, index| - record.send("#{column}=", ciphertexts[index]) - record.save - end + Vault::PerformInBatches.new(attribute, options).encrypt(records, plaintexts) end # works only with convergent encryption @@ -186,25 +170,7 @@ def vault_persist_all(attribute, records, plaintexts) def vault_load_all(attribute, records) options = __vault_attributes[attribute] - key = options[:key] - path = options[:path] - serializer = options[:serializer] - column = options[:encrypted_column] - - # Load the ciphertext - ciphertexts = records.map { |record| record.read_attribute(column) } - - # Load the plaintext value - plaintexts = Vault::Rails.batch_decrypt(path, key, ciphertexts, Vault.client) - - # Deserialize the plaintext values, if a serializer exists - if serializer - plaintexts = plaintexts.map { |plaintext| serializer.decode(plaintext) } - end - - records.each_with_index do |record, index| - record.instance_variable_set("@#{attribute}", plaintexts[index]) - end + Vault::PerformInBatches.new(attribute, options).decrypt(records) end end diff --git a/lib/vault/perform_in_batches.rb b/lib/vault/perform_in_batches.rb new file mode 100644 index 00000000..32943aa7 --- /dev/null +++ b/lib/vault/perform_in_batches.rb @@ -0,0 +1,55 @@ +module Vault + class PerformInBatches + def initialize(attribute, options) + @attribute = attribute + + @key = options[:key] + @path = options[:path] + @serializer = options[:serializer] + @column = options[:encrypted_column] + @convergent = options[:convergent] + end + + def encrypt(records, plaintexts) + raise 'Batch Operations work only with convergent attributes' unless @convergent + + raw_plaintexts = serialize(plaintexts) + + ciphertexts = Vault::Rails.batch_encrypt(path, key, raw_plaintexts, Vault.client) + + records.each_with_index do |record, index| + record.send("#{column}=", ciphertexts[index]) + record.save + end + end + + def decrypt(records) + raise 'Batch Operations work only with convergent attributes' unless @convergent + + ciphertexts = records.map { |record| record.send(column) } + + raw_plaintexts = Vault::Rails.batch_decrypt(path, key, ciphertexts, Vault.client) + plaintexts = deserialize(raw_plaintexts) + + records.each_with_index do |record, index| + record.instance_variable_set("@#{attribute}", plaintexts[index]) + end + end + + private + + attr_reader :key, :path, :serializer, :column, :attribute + + def serialize(plaintexts) + return plaintexts unless serializer + + plaintexts.map { |plaintext| serializer.encode(plaintext) } + end + + def deserialize(plaintexts) + return plaintexts unless serializer + + plaintexts.map { |plaintext| serializer.decode(plaintext) } + end + end +end diff --git a/lib/vault/rails.rb b/lib/vault/rails.rb index a8df5b70..887f710b 100644 --- a/lib/vault/rails.rb +++ b/lib/vault/rails.rb @@ -5,6 +5,7 @@ require_relative 'encrypted_model' require_relative 'attribute_proxy' +require_relative 'perform_in_batches' require_relative 'rails/configurable' require_relative 'rails/errors' require_relative 'rails/serializers/json_serializer' diff --git a/spec/unit/perform_in_batches_spec.rb b/spec/unit/perform_in_batches_spec.rb new file mode 100644 index 00000000..4e6bbaa8 --- /dev/null +++ b/spec/unit/perform_in_batches_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' + +describe Vault::PerformInBatches do + describe '#encrypt' do + context 'non-convergent attribute' do + let(:options) do + { + key: 'test_key', + path: 'test_path', + column: 'test_attribute_encrypted', + convergent: false + } + end + + it 'raises an exception for non-convergent attributes' do + attribute = 'test_attribute' + records = [double(:first_object, save: true), double(:second_object, save: true)] + plaintexts = %w(plaintext1 plaintext2) + + expect do + Vault::PerformInBatches.new(attribute, options).encrypt(records, plaintexts) + end.to raise_error 'Batch Operations work only with convergent attributes' + end + end + + context 'convergent attribute' do + let(:options) do + { + key: 'test_key', + path: 'test_path', + encrypted_column: 'test_attribute_encrypted', + convergent: true + } + end + + it 'encrypts one attribute for a batch of records and saves it' do + attribute = 'test_attribute' + + first_record = double(save: true) + second_record = double(save: true) + records = [first_record, second_record] + + plaintexts = %w(plaintext1 plaintext2) + + + expect(Vault::Rails).to receive(:batch_encrypt) + .with('test_path', 'test_key', %w(plaintext1 plaintext2), Vault.client) + .and_return(%w(ciphertext1 ciphertext2)) + + expect(first_record).to receive('test_attribute_encrypted=').with('ciphertext1') + expect(second_record).to receive('test_attribute_encrypted=').with('ciphertext2') + + Vault::PerformInBatches.new(attribute, options).encrypt(records, plaintexts) + end + + context 'with given serializer' do + let(:options) do + { + key: 'test_key', + path: 'test_path', + encrypted_column: 'test_attribute_encrypted', + serializer: Vault::Rails::Serializers::IntegerSerializer, + convergent: true + } + end + + it 'encrypts one attribute for a batch of records and saves it' do + attribute = 'test_attribute' + + first_record = double(save: true) + second_record = double(save: true) + records = [first_record, second_record] + + plaintexts = [100, 200] + + expect(Vault::Rails).to receive(:batch_encrypt) + .with('test_path', 'test_key', %w(100 200), Vault.client) + .and_return(%w(ciphertext1 ciphertext2)) + + expect(first_record).to receive('test_attribute_encrypted=').with('ciphertext1') + expect(second_record).to receive('test_attribute_encrypted=').with('ciphertext2') + + Vault::PerformInBatches.new(attribute, options).encrypt(records, plaintexts) + end + end + end + end + + describe '#decrypt' do + context 'non-convergent attribute' do + let(:options) do + { + key: 'test_key', + path: 'test_path', + column: 'test_attribute_encrypted', + convergent: false + } + end + + it 'raises an exception for non-convergent attributes' do + attribute = 'test_attribute' + records = [double(:first_object, save: true), double(:second_object, save: true)] + plaintexts = %w(plaintext1 plaintext2) + + expect do + Vault::PerformInBatches.new(attribute, options).encrypt(records, plaintexts) + end.to raise_error 'Batch Operations work only with convergent attributes' + end + end + + context 'convergent attribute' do + let(:options) do + { + key: 'test_key', + path: 'test_path', + encrypted_column: 'test_attribute_encrypted', + convergent: true + } + end + + it 'decrypts one attribute for a batch of records and loads it' do + attribute = 'test_attribute' + + first_record = double(test_attribute_encrypted: 'ciphertext1') + second_record = double(test_attribute_encrypted: 'ciphertext2') + records = [first_record, second_record] + + expect(Vault::Rails).to receive(:batch_decrypt) + .with('test_path', 'test_key', %w(ciphertext1 ciphertext2), Vault.client) + .and_return(%w(plaintext1 plaintext2)) + + expect(first_record).to receive('instance_variable_set').with('@test_attribute', 'plaintext1') + expect(second_record).to receive('instance_variable_set').with('@test_attribute', 'plaintext2') + + Vault::PerformInBatches.new(attribute, options).decrypt(records) + end + + context 'with given serializer' do + let(:options) do + { + key: 'test_key', + path: 'test_path', + encrypted_column: 'test_attribute_encrypted', + serializer: Vault::Rails::Serializers::IntegerSerializer, + convergent: true + } + end + + it 'decrypts one attribute for a batch of records and loads it' do + attribute = 'test_attribute' + + first_record = double(test_attribute_encrypted: 'ciphertext1') + second_record = double(test_attribute_encrypted: 'ciphertext2') + records = [first_record, second_record] + + expect(Vault::Rails).to receive(:batch_decrypt) + .with('test_path', 'test_key', %w(ciphertext1 ciphertext2), Vault.client) + .and_return(%w(100 200)) + + expect(first_record).to receive('instance_variable_set').with('@test_attribute', 100) + expect(second_record).to receive('instance_variable_set').with('@test_attribute', 200) + + Vault::PerformInBatches.new(attribute, options).decrypt(records) + end + end + end + end +end