diff --git a/CHANGELOG.md b/CHANGELOG.md index ee2ca26..113cdef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Vault Rails Changelog +## 2.2.0 (June 28, 2024) +* Adds `TransitJsonCodec.batch_*` functions which use the Vault batch API to process + an array of values with one API call. + * like `Vault::Rails.batch_decrypt` but with `transit` and FC's standard JSON pre-encoding of values. + ## 2.1.2 (November 17, 2023) IMPROVEMENTS diff --git a/lib/vault/rails/version.rb b/lib/vault/rails/version.rb index 1c07fb7..ebd63e1 100644 --- a/lib/vault/rails/version.rb +++ b/lib/vault/rails/version.rb @@ -1,6 +1,6 @@ module Vault module Rails - VERSION = '2.1.2' + VERSION = '2.2.0' def self.latest? ActiveRecord.version >= Gem::Version.new('5.0.0') diff --git a/lib/vault/transit_json_codec.rb b/lib/vault/transit_json_codec.rb index 5c32bbb..869ec02 100644 --- a/lib/vault/transit_json_codec.rb +++ b/lib/vault/transit_json_codec.rb @@ -18,6 +18,17 @@ def encrypt(plaintext) secret.data[:ciphertext] end + def batch_encrypt(plaintexts) + return [] if plaintexts.blank? + + secrets = Vault.logical.write( + "transit/encrypt/#{key}", + batch_input: plaintexts.map { |plaintext| { plaintext: Base64.strict_encode64(Oj.dump(plaintext)) } } + ) + + secrets.data[:batch_results].map { |result| result[:ciphertext] } + end + def decrypt(ciphertext) return if ciphertext.blank? @@ -29,6 +40,17 @@ def decrypt(ciphertext) Oj.load(Base64.strict_decode64(secret.data[:plaintext])) end + def batch_decrypt(ciphertexts) + return [] if ciphertexts.blank? + + secret = Vault.logical.write( + "transit/decrypt/#{key}", + batch_input: ciphertexts.map { |ciphertext| { ciphertext: ciphertext } } + ) + + secret.data[:batch_results].map { |result| Oj.load(Base64.strict_decode64(result[:plaintext])) } + end + private attr_reader :key diff --git a/spec/unit/transit_json_codec_spec.rb b/spec/unit/transit_json_codec_spec.rb index a1e6073..8ea401d 100644 --- a/spec/unit/transit_json_codec_spec.rb +++ b/spec/unit/transit_json_codec_spec.rb @@ -85,4 +85,67 @@ end end end + + describe '#batch_encrypt' do + context 'when plaintexts array is empty' do + it 'returns empty array' do + expect(codec.batch_encrypt([])).to eq([]) + expect(codec.batch_encrypt(nil)).to eq([]) + end + end + + context 'when plaintexts are present' do + let(:plaintexts) { ['some text', 'other text'] } + + it 'returns array of encrypted values' do + ciphertexts = codec.batch_encrypt(plaintexts) + expect(ciphertexts).not_to be_blank + expect(ciphertexts.length).to eq(plaintexts.length) + ciphertexts.each { |ciphertext| expect(ciphertext).to start_with('vault:v1:') } + ciphertexts.each_with_index { |ciphertext, i| expect(codec.decrypt(ciphertext)).to eq(plaintexts[i]) } + end + + context 'when encryption fails' do + before do + allow(Vault).to receive(:logical).and_raise(StandardError, 'Oh no!') + end + + it 're-raises error' do + expect { codec.batch_encrypt(plaintexts) }.to raise_error(StandardError) + end + end + end + end + + describe '#batch_decrypt' do + context 'when ciphertexts array is empty' do + it 'returns empty array' do + expect(codec.batch_decrypt([])).to eq([]) + expect(codec.batch_decrypt(nil)).to eq([]) + end + end + + context 'when ciphertexts are present' do + let(:plaintexts) { ['some text', 'other text'] } + let(:ciphertexts) { codec.batch_encrypt(plaintexts) } + + it 'returns array of decrypted values' do + decrypted = codec.batch_decrypt(ciphertexts) + expect(decrypted).not_to be_blank + expect(decrypted.length).to eq(plaintexts.length) + expect(decrypted).to eq(plaintexts) + end + + context 'when decryption fails' do + before do + allow(Vault).to receive(:logical).and_raise(StandardError, 'Oh no!') + end + + it 're-raises error' do + expect { codec.batch_decrypt(plaintexts) }.to raise_error(StandardError) + end + end + end + end + end