attr_vault 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +36 -0
- data/LICENSE +22 -0
- data/attr_vault.gemspec +23 -0
- data/lib/attr_vault/cryptor.rb +57 -0
- data/lib/attr_vault/encryption.rb +70 -0
- data/lib/attr_vault/errors.rb +22 -0
- data/lib/attr_vault/keyring.rb +85 -0
- data/lib/attr_vault/secret.rb +48 -0
- data/lib/attr_vault/version.rb +3 -0
- data/lib/attr_vault.rb +129 -0
- data/spec/attr_vault/keyring_spec.rb +184 -0
- data/spec/attr_vault/secret_spec.rb +34 -0
- data/spec/attr_vault_spec.rb +328 -0
- data/spec/spec_helper.rb +49 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 93ec67ae603b025fd34c27c9b769cdfd6d6814c8
|
4
|
+
data.tar.gz: 3c7af4318feb415e45dba7cb176612ba9db59c28
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a77d9447d153c91af3f033e4dce072eaa08e5189d150cf2cde0715592493fafc899020e6299c0c0f7e70042a8906ceb6b77c9855dba9ce43d4a7924aab7f5fe2
|
7
|
+
data.tar.gz: a9ae10282b3f5a21ecce80f2a70602995fb13ccce26c7fb7c758446f2b54069ed108f1619f6bdb8f6e30b76e16ab69386141848b5906a89aeee7e34c13ec87b9
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
attr_vault (0.0.1)
|
5
|
+
fernet (~> 2.1)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
diff-lcs (1.2.5)
|
11
|
+
fernet (2.1)
|
12
|
+
valcro (= 0.1)
|
13
|
+
pg (0.17.1)
|
14
|
+
rspec (3.1.0)
|
15
|
+
rspec-core (~> 3.1.0)
|
16
|
+
rspec-expectations (~> 3.1.0)
|
17
|
+
rspec-mocks (~> 3.1.0)
|
18
|
+
rspec-core (3.1.7)
|
19
|
+
rspec-support (~> 3.1.0)
|
20
|
+
rspec-expectations (3.1.2)
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
22
|
+
rspec-support (~> 3.1.0)
|
23
|
+
rspec-mocks (3.1.3)
|
24
|
+
rspec-support (~> 3.1.0)
|
25
|
+
rspec-support (3.1.2)
|
26
|
+
sequel (4.13.0)
|
27
|
+
valcro (0.1)
|
28
|
+
|
29
|
+
PLATFORMS
|
30
|
+
ruby
|
31
|
+
|
32
|
+
DEPENDENCIES
|
33
|
+
attr_vault!
|
34
|
+
pg
|
35
|
+
rspec (~> 3.0)
|
36
|
+
sequel (~> 4.13.0)
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Maciek Sakrejda
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/attr_vault.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require File.expand_path('../lib/attr_vault/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Maciek Sakrejda"]
|
5
|
+
gem.email = ["m.sakrejda@gmail.com"]
|
6
|
+
gem.description = %q{Encryption at rest made easy}
|
7
|
+
gem.summary = %q{Sequel plugin for encryption at rest}
|
8
|
+
gem.homepage = "https://github.com/deafbybeheading/attr_vault"
|
9
|
+
|
10
|
+
gem.files = `git ls-files`.split($\)
|
11
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.name = "attr_vault"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = AttrVault::VERSION
|
16
|
+
gem.license = "MIT"
|
17
|
+
|
18
|
+
gem.add_runtime_dependency 'fernet', '~> 2.1'
|
19
|
+
|
20
|
+
gem.add_development_dependency "rspec", '~> 3.0'
|
21
|
+
gem.add_development_dependency "pg", '~> 0'
|
22
|
+
gem.add_development_dependency "sequel", '~> 4.13'
|
23
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module AttrVault
|
2
|
+
module Cryptor
|
3
|
+
def self.encrypt(value, key)
|
4
|
+
return [nil, nil] if value.nil?
|
5
|
+
return ['', ''] if value.empty?
|
6
|
+
|
7
|
+
secret = AttrVault::Secret.new(key)
|
8
|
+
|
9
|
+
encrypted_message, iv = Encryption.encrypt(
|
10
|
+
key: secret.encryption_key,
|
11
|
+
message: value
|
12
|
+
)
|
13
|
+
|
14
|
+
encrypted_payload = iv + encrypted_message
|
15
|
+
mac = OpenSSL::HMAC.digest('sha256', secret.signing_key, encrypted_payload)
|
16
|
+
[ encrypted_payload, mac ]
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.decrypt(encrypted_payload, hmac, key)
|
20
|
+
return nil if encrypted_payload.nil? && hmac.nil?
|
21
|
+
return '' if encrypted_payload.empty? && hmac.empty?
|
22
|
+
|
23
|
+
secret = AttrVault::Secret.new(key)
|
24
|
+
|
25
|
+
expected_hmac = Encryption.hmac_digest(secret.signing_key, encrypted_payload)
|
26
|
+
unless verify_signature(expected_hmac, hmac)
|
27
|
+
raise InvalidCiphertext, "Expected hmac #{expected_hmac} for this value; got #{hmac}"
|
28
|
+
end
|
29
|
+
|
30
|
+
iv, encrypted_message = encrypted_payload[0..16], encrypted_payload[16..-1]
|
31
|
+
|
32
|
+
block_size = Encryption::AES_BLOCK_SIZE
|
33
|
+
unless (encrypted_message.size % block_size).zero?
|
34
|
+
raise InvalidCiphertext,
|
35
|
+
"Expected message size to be multiple of #{block_size}; got #{encrypted_message.size}"
|
36
|
+
end
|
37
|
+
|
38
|
+
begin
|
39
|
+
Encryption.decrypt(key: secret.encryption_key,
|
40
|
+
ciphertext: encrypted_message,
|
41
|
+
iv: iv)
|
42
|
+
rescue OpenSSL::Cipher::CipherError
|
43
|
+
raise InvalidCiphertext, "Could not decrypt field"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def self.verify_signature(expected, actual)
|
50
|
+
expected_bytes = expected.bytes.to_a
|
51
|
+
actual_bytes = actual.bytes.to_a
|
52
|
+
actual_bytes.inject(0) do |accum, byte|
|
53
|
+
accum |= byte ^ expected_bytes.shift
|
54
|
+
end.zero?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module AttrVault
|
4
|
+
# borrowed wholesale from Fernet
|
5
|
+
|
6
|
+
# Internal: Encapsulates encryption and signing primitives
|
7
|
+
module Encryption
|
8
|
+
AES_BLOCK_SIZE = 16.freeze
|
9
|
+
|
10
|
+
# Internal: Encrypts the provided message using a AES-128-CBC cipher with a
|
11
|
+
# random IV and the provided encryption key
|
12
|
+
#
|
13
|
+
# Arguments:
|
14
|
+
#
|
15
|
+
# * message - the message to encrypt
|
16
|
+
# * key - the encryption key
|
17
|
+
# * iv - override for the random IV, only used for testing
|
18
|
+
#
|
19
|
+
# Examples
|
20
|
+
#
|
21
|
+
# ciphertext, iv = AttrVault::Encryption.encrypt(
|
22
|
+
# message: 'this is a secret', key: encryption_key
|
23
|
+
# )
|
24
|
+
#
|
25
|
+
# Returns a two-element array containing the ciphertext and the random IV
|
26
|
+
def self.encrypt(key:, message:, iv: nil)
|
27
|
+
cipher = OpenSSL::Cipher.new('AES-128-CBC')
|
28
|
+
cipher.encrypt
|
29
|
+
iv ||= cipher.random_iv
|
30
|
+
cipher.iv = iv
|
31
|
+
cipher.key = key
|
32
|
+
[cipher.update(message) + cipher.final, iv]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Internal: Decrypts the provided ciphertext using a AES-128-CBC cipher with a
|
36
|
+
# the provided IV and encryption key
|
37
|
+
#
|
38
|
+
# Arguments:
|
39
|
+
#
|
40
|
+
# * ciphertext - encrypted message
|
41
|
+
# * key - encryption key used to encrypt the message
|
42
|
+
# * iv - initialization vector used in the ciphertext's cipher
|
43
|
+
#
|
44
|
+
# Examples
|
45
|
+
#
|
46
|
+
# ciphertext, iv = AttrVault::Encryption.encrypt(
|
47
|
+
# message: 'this is a secret', key: encryption_key
|
48
|
+
# )
|
49
|
+
#
|
50
|
+
# Returns a two-element array containing the ciphertext and the random IV
|
51
|
+
def self.decrypt(key:, ciphertext:, iv:)
|
52
|
+
decipher = OpenSSL::Cipher.new('AES-128-CBC')
|
53
|
+
decipher.decrypt
|
54
|
+
decipher.iv = iv
|
55
|
+
decipher.key = key
|
56
|
+
decipher.update(ciphertext) + decipher.final
|
57
|
+
end
|
58
|
+
|
59
|
+
# Internal: Creates an HMAC signature (sha256 hashing) of the given bytes
|
60
|
+
# with the provided signing key
|
61
|
+
#
|
62
|
+
# key - the signing key
|
63
|
+
# bytes - blob of bytes to sign
|
64
|
+
#
|
65
|
+
# Returns the HMAC signature as a string
|
66
|
+
def self.hmac_digest(key, bytes)
|
67
|
+
OpenSSL::HMAC.digest('sha256', key, bytes)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module AttrVault
|
2
|
+
# Base class for AttrVault errors
|
3
|
+
class Error < StandardError; end
|
4
|
+
class InvalidSecret < AttrVault::Error; end
|
5
|
+
class InvalidKey < AttrVault::Error; end
|
6
|
+
class InvalidKeyring < AttrVault::Error; end
|
7
|
+
class KeyringEmpty < AttrVault::Error; end
|
8
|
+
class UnknownKey < AttrVault::Error
|
9
|
+
def initialize(key_id)
|
10
|
+
@key_id = key_id
|
11
|
+
end
|
12
|
+
def message
|
13
|
+
formatted_id = if @key_id.nil?
|
14
|
+
'<nil>'
|
15
|
+
else
|
16
|
+
@key_id
|
17
|
+
end
|
18
|
+
"No key with id #{formatted_id} found in keyring"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
class InvalidCiphertext < AttrVault::Error; end
|
22
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module AttrVault
|
2
|
+
class Key
|
3
|
+
attr_reader :id, :value, :created_at
|
4
|
+
|
5
|
+
def initialize(id, value, created_at)
|
6
|
+
if id.nil?
|
7
|
+
raise InvalidKey, "key id required"
|
8
|
+
end
|
9
|
+
if value.nil?
|
10
|
+
raise InvalidKey, "key value required"
|
11
|
+
end
|
12
|
+
if created_at.nil?
|
13
|
+
raise InvalidKey, "key created_at required"
|
14
|
+
end
|
15
|
+
|
16
|
+
@id = id
|
17
|
+
@value = value
|
18
|
+
@created_at = created_at
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_json(*args)
|
22
|
+
{ id: id, value: value, created_at: created_at }.to_json
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Keyring
|
27
|
+
attr_reader :keys
|
28
|
+
|
29
|
+
def self.load(keyring_data)
|
30
|
+
keyring = Keyring.new
|
31
|
+
begin
|
32
|
+
candidate_keys = JSON.parse(keyring_data)
|
33
|
+
unless candidate_keys.respond_to? :each
|
34
|
+
raise InvalidKeyring, "does not respond to each"
|
35
|
+
end
|
36
|
+
candidate_keys.each_with_index do |k|
|
37
|
+
created_at = unless k["created_at"].nil?
|
38
|
+
Time.parse(k["created_at"])
|
39
|
+
end
|
40
|
+
keyring.add_key(Key.new(k["id"], k["value"], created_at || Time.now))
|
41
|
+
end
|
42
|
+
rescue StandardError => e
|
43
|
+
raise InvalidKeyring, e.message
|
44
|
+
end
|
45
|
+
keyring
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize
|
49
|
+
@keys = []
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_key(k)
|
53
|
+
@keys << k
|
54
|
+
end
|
55
|
+
|
56
|
+
def drop_key(id_or_key)
|
57
|
+
id = if id_or_key.is_a? Key
|
58
|
+
id_or_key.id
|
59
|
+
else
|
60
|
+
id_or_key
|
61
|
+
end
|
62
|
+
@keys.reject! { |k| k.id == id }
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch(id)
|
66
|
+
@keys.find { |k| k.id == id } or raise UnknownKey, id
|
67
|
+
end
|
68
|
+
|
69
|
+
def has_key?(id)
|
70
|
+
!@keys.find { |k| k.id == id }.nil?
|
71
|
+
end
|
72
|
+
|
73
|
+
def current_key
|
74
|
+
k = @keys.sort_by(&:created_at).last
|
75
|
+
if k.nil?
|
76
|
+
raise KeyringEmpty, "No keys in keyring"
|
77
|
+
end
|
78
|
+
k
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_json
|
82
|
+
@keys.to_json
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module AttrVault
|
4
|
+
# borrowed wholesale from Fernet
|
5
|
+
|
6
|
+
# Internal: Encapsulates a secret key, a 32-byte sequence consisting
|
7
|
+
# of an encryption and a signing key.
|
8
|
+
class Secret
|
9
|
+
# Internal - Initialize a Secret
|
10
|
+
#
|
11
|
+
# secret - the secret, optionally encoded with either standard or
|
12
|
+
# URL safe variants of Base64 encoding
|
13
|
+
#
|
14
|
+
# Raises AttrVault::Secret::InvalidSecret if it cannot be decoded or is
|
15
|
+
# not of the expected length
|
16
|
+
def initialize(secret)
|
17
|
+
if secret.bytesize == 32
|
18
|
+
@secret = secret
|
19
|
+
else
|
20
|
+
begin
|
21
|
+
@secret = Base64.urlsafe_decode64(secret)
|
22
|
+
rescue ArgumentError
|
23
|
+
@secret = Base64.decode64(secret)
|
24
|
+
end
|
25
|
+
unless @secret.bytesize == 32
|
26
|
+
raise InvalidSecret,
|
27
|
+
"Secret must be 32 bytes, instead got #{@secret.bytesize}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Internal: Returns the portion of the secret token used for encryption
|
33
|
+
def encryption_key
|
34
|
+
@secret.slice(16, 16)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Internal: Returns the portion of the secret token used for signing
|
38
|
+
def signing_key
|
39
|
+
@secret.slice(0, 16)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: String representation of this secret, masks to avoid leaks.
|
43
|
+
def to_s
|
44
|
+
"<AttrVault::Secret [masked]>"
|
45
|
+
end
|
46
|
+
alias to_s inspect
|
47
|
+
end
|
48
|
+
end
|
data/lib/attr_vault.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'attr_vault/errors'
|
2
|
+
require 'attr_vault/keyring'
|
3
|
+
require 'attr_vault/secret'
|
4
|
+
require 'attr_vault/encryption'
|
5
|
+
require 'attr_vault/cryptor'
|
6
|
+
|
7
|
+
module AttrVault
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
base.include(InstanceMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
def before_save
|
15
|
+
keyring = self.class.vault_keys
|
16
|
+
current_key = keyring.current_key
|
17
|
+
key_id = self[self.class.vault_key_field]
|
18
|
+
record_key = self.class.vault_keys.fetch(key_id) unless key_id.nil?
|
19
|
+
|
20
|
+
@vault_dirty_attrs ||= {}
|
21
|
+
if !record_key.nil? && current_key != record_key
|
22
|
+
# If the record key is not nil and not current, flag *all*
|
23
|
+
# attrs as dirty, since we want to rewrite them all in order
|
24
|
+
# to use the latest key. Note that when the record key is nil,
|
25
|
+
# we're dealing with a new record, so there are no existing
|
26
|
+
# vault attributes to rewrite. We only write these out when
|
27
|
+
# they're set explicitly in a new record, in which case they
|
28
|
+
# will be in the dirty attrs already and are handled below.
|
29
|
+
self.class.vault_attrs.each do |attr|
|
30
|
+
next if @vault_dirty_attrs.has_key? attr.name
|
31
|
+
@vault_dirty_attrs[attr.name] = self.send(attr.name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
# If any attr has plaintext_source_field and the plaintext field
|
35
|
+
# has a value set, flag the attr as dirty using the plaintext
|
36
|
+
# source value, then nil out the plaintext field.
|
37
|
+
self.class.vault_attrs.reject { |attr| attr.plaintext_source_field.nil? }.each do |attr|
|
38
|
+
unless self[attr.plaintext_source_field].nil?
|
39
|
+
@vault_dirty_attrs[attr.name] = self[attr.plaintext_source_field]
|
40
|
+
self[attr.plaintext_source_field] = nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
self.class.vault_attrs.each do |attr|
|
44
|
+
next unless @vault_dirty_attrs.has_key? attr.name
|
45
|
+
|
46
|
+
value = @vault_dirty_attrs[attr.name]
|
47
|
+
encrypted, hmac = Cryptor.encrypt(value, current_key.value)
|
48
|
+
|
49
|
+
unless encrypted.nil?
|
50
|
+
encrypted = Sequel.blob(encrypted)
|
51
|
+
end
|
52
|
+
unless hmac.nil?
|
53
|
+
hmac = Sequel.blob(hmac)
|
54
|
+
end
|
55
|
+
|
56
|
+
self[attr.encrypted_field] = encrypted
|
57
|
+
self[attr.hmac_field] = hmac
|
58
|
+
end
|
59
|
+
self[self.class.vault_key_field] = current_key.id
|
60
|
+
@vault_dirty_attrs = {}
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module ClassMethods
|
66
|
+
def vault_keyring(keyring_data, key_field: :key_id)
|
67
|
+
@key_field = key_field.to_sym
|
68
|
+
@keyring = Keyring.load(keyring_data)
|
69
|
+
end
|
70
|
+
|
71
|
+
def vault_attr(name, opts={})
|
72
|
+
attr = VaultAttr.new(name, opts)
|
73
|
+
self.vault_attrs << attr
|
74
|
+
|
75
|
+
define_method(name) do
|
76
|
+
# if there is a plaintext source field, use that and ignore
|
77
|
+
# the encrypted field
|
78
|
+
if !attr.plaintext_source_field.nil? && !self[attr.plaintext_source_field].nil?
|
79
|
+
return self[attr.plaintext_source_field]
|
80
|
+
end
|
81
|
+
|
82
|
+
keyring = self.class.vault_keys
|
83
|
+
key_id = self[self.class.vault_key_field]
|
84
|
+
record_key = self.class.vault_keys.fetch(key_id)
|
85
|
+
|
86
|
+
encrypted_value = self[attr.encrypted_field]
|
87
|
+
hmac = self[attr.hmac_field]
|
88
|
+
# TODO: cache decrypted value
|
89
|
+
Cryptor.decrypt(encrypted_value, hmac, record_key.value)
|
90
|
+
end
|
91
|
+
|
92
|
+
define_method("#{name}=") do |value|
|
93
|
+
@vault_dirty_attrs ||= {}
|
94
|
+
@vault_dirty_attrs[name] = value
|
95
|
+
# ensure that Sequel knows that this is in fact dirty and must
|
96
|
+
# be updated--otherwise, the object is never saved,
|
97
|
+
# #before_save is never called, and we never store the update
|
98
|
+
self.modified! attr.encrypted_field
|
99
|
+
self.modified! attr.hmac_field
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def vault_attrs
|
104
|
+
@vault_attrs ||= []
|
105
|
+
end
|
106
|
+
|
107
|
+
def vault_key_field
|
108
|
+
@key_field
|
109
|
+
end
|
110
|
+
|
111
|
+
def vault_keys
|
112
|
+
@keyring
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class VaultAttr
|
117
|
+
attr_reader :name, :encrypted_field, :hmac_field, :plaintext_source_field
|
118
|
+
|
119
|
+
def initialize(name,
|
120
|
+
encrypted_field: "#{name}_encrypted",
|
121
|
+
hmac_field: "#{name}_hmac",
|
122
|
+
plaintext_source_field: nil)
|
123
|
+
@name = name
|
124
|
+
@encrypted_field = encrypted_field.to_sym
|
125
|
+
@hmac_field = hmac_field.to_sym
|
126
|
+
@plaintext_source_field = plaintext_source_field.to_sym unless plaintext_source_field.nil?
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module AttrVault
|
5
|
+
describe Keyring do
|
6
|
+
|
7
|
+
describe ".load" do
|
8
|
+
let(:key_data) {
|
9
|
+
[
|
10
|
+
{ id: SecureRandom.uuid, value: SecureRandom.base64(32), created_at: Time.now },
|
11
|
+
{ id: SecureRandom.uuid, value: SecureRandom.base64(32), created_at: Time.now }
|
12
|
+
]
|
13
|
+
}
|
14
|
+
|
15
|
+
it "loads a valid keyring string" do
|
16
|
+
keyring = Keyring.load(key_data.to_json)
|
17
|
+
expect(keyring).to be_a Keyring
|
18
|
+
expect(keyring.keys.count).to eq 2
|
19
|
+
(0..1).each do |i|
|
20
|
+
expect(keyring.keys[i].id).to eq key_data[i][:id]
|
21
|
+
expect(keyring.keys[i].value).to eq key_data[i][:value]
|
22
|
+
expect(keyring.keys[i].created_at).to be_within(60).of(key_data[i][:created_at])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it "rejects unexpected JSON" do
|
27
|
+
expect { Keyring.load('hello') }.to raise_error(InvalidKeyring)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "rejects unknown formats" do
|
31
|
+
keys = key_data.map do |k|
|
32
|
+
"<key id='#{k[:id]}' value='#{k[:value]}' created_at='#{k[:created_at]}'/>"
|
33
|
+
end
|
34
|
+
expect { Keyring.load("<keys>#{keys}</keys>") }.to raise_error(InvalidKeyring)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "rejects keys with missing ids" do
|
38
|
+
key_data[0].delete :id
|
39
|
+
expect { Keyring.load(key_data) }.to raise_error(InvalidKeyring)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "rejects keys with missing values" do
|
43
|
+
key_data[0].delete :value
|
44
|
+
expect { Keyring.load(key_data) }.to raise_error(InvalidKeyring)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#keys" do
|
50
|
+
let(:keyring) { Keyring.new }
|
51
|
+
let(:k1) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
52
|
+
let(:k2) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
53
|
+
|
54
|
+
before do
|
55
|
+
keyring.add_key(k1)
|
56
|
+
keyring.add_key(k2)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "lists all keys" do
|
60
|
+
expect(keyring.keys).to include(k1)
|
61
|
+
expect(keyring.keys).to include(k2)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#fetch" do
|
66
|
+
let(:keyring) { Keyring.new }
|
67
|
+
let(:k1) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
68
|
+
let(:k2) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
69
|
+
|
70
|
+
before do
|
71
|
+
keyring.add_key(k1)
|
72
|
+
keyring.add_key(k2)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "finds the right key by its id" do
|
76
|
+
expect(keyring.fetch(k1.id)).to be k1
|
77
|
+
expect(keyring.fetch(k2.id)).to be k2
|
78
|
+
end
|
79
|
+
|
80
|
+
it "raises for an unknown id" do
|
81
|
+
expect { keyring.fetch('867344d2-ac73-493b-9a9e-5fa688ba25ef') }
|
82
|
+
.to raise_error(UnknownKey)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "#has_key?" do
|
87
|
+
let(:keyring) { Keyring.new }
|
88
|
+
let(:k1) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
89
|
+
let(:k2) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
90
|
+
|
91
|
+
before do
|
92
|
+
keyring.add_key(k1)
|
93
|
+
keyring.add_key(k2)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "is true if the keyring has a key with the given id" do
|
97
|
+
expect(keyring.has_key?(k1.id)).to be true
|
98
|
+
expect(keyring.has_key?(k2.id)).to be true
|
99
|
+
end
|
100
|
+
|
101
|
+
it "is false if no such key is present" do
|
102
|
+
expect(keyring.has_key?('867344d2-ac73-493b-9a9e-5fa688ba25ef')).to be false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "#add_key" do
|
107
|
+
let(:keyring) { Keyring.new }
|
108
|
+
let(:k1) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
109
|
+
|
110
|
+
it "adds keys" do
|
111
|
+
expect(keyring.keys).to be_empty
|
112
|
+
expect { keyring.add_key(k1) }.to change { keyring.keys.count }.by 1
|
113
|
+
expect(keyring.keys[0]).to be k1
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "#drop_key" do
|
118
|
+
let(:keyring) { Keyring.new }
|
119
|
+
let(:k1) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
120
|
+
let(:k2) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
121
|
+
|
122
|
+
before do
|
123
|
+
keyring.add_key(k1)
|
124
|
+
keyring.add_key(k2)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "drops keys by identity" do
|
128
|
+
expect(keyring.keys.count).to eq 2
|
129
|
+
expect { keyring.drop_key(k1) }.to change { keyring.keys.count }.by -1
|
130
|
+
expect(keyring.keys.count).to eq 1
|
131
|
+
expect(keyring.keys[0]).to be k2
|
132
|
+
end
|
133
|
+
|
134
|
+
it "drops keys by identifier" do
|
135
|
+
expect(keyring.keys.count).to eq 2
|
136
|
+
expect { keyring.drop_key(k1.id) }.to change { keyring.keys.count }.by -1
|
137
|
+
expect(keyring.keys.count).to eq 1
|
138
|
+
expect(keyring.keys[0]).to be k2
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "#to_json" do
|
143
|
+
let(:keyring) { Keyring.new }
|
144
|
+
let(:k1) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
145
|
+
let(:k2) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
146
|
+
|
147
|
+
before do
|
148
|
+
keyring.add_key(k1)
|
149
|
+
keyring.add_key(k2)
|
150
|
+
end
|
151
|
+
|
152
|
+
it "serializes the keyring to an expected format" do
|
153
|
+
keyring_data = keyring.to_json
|
154
|
+
reparsed = JSON.parse(keyring_data)
|
155
|
+
expect(reparsed[0]["id"]).to eq k1.id
|
156
|
+
expect(reparsed[0]["value"]).to eq k1.value
|
157
|
+
expect(reparsed[0]["created_at"]).to eq k1.created_at.to_s
|
158
|
+
|
159
|
+
expect(reparsed[1]["id"]).to eq k2.id
|
160
|
+
expect(reparsed[1]["value"]).to eq k2.value
|
161
|
+
expect(reparsed[1]["created_at"]).to eq k2.created_at.to_s
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "#current_key" do
|
166
|
+
let(:keyring) { Keyring.new }
|
167
|
+
let(:k1) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now - 3) }
|
168
|
+
let(:k2) { Key.new(SecureRandom.uuid, SecureRandom.base64(32), Time.now) }
|
169
|
+
|
170
|
+
before do
|
171
|
+
keyring.add_key(k1)
|
172
|
+
keyring.add_key(k2)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "returns the newest key" do
|
176
|
+
expect(keyring.current_key).to eq k2
|
177
|
+
end
|
178
|
+
|
179
|
+
it "raise if no keys are registered" do
|
180
|
+
other_keyring = Keyring.new
|
181
|
+
expect { other_keyring.current_key }.to raise_error(KeyringEmpty)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AttrVault::Secret do
|
4
|
+
it "can resolve a URL safe base64 encoded 32 byte string" do
|
5
|
+
resolves_input(Base64.urlsafe_encode64("A"*16 + "B"*16))
|
6
|
+
end
|
7
|
+
|
8
|
+
it "can resolve a base64 encoded 32 byte string" do
|
9
|
+
resolves_input(Base64.encode64("A"*16 + "B"*16))
|
10
|
+
end
|
11
|
+
|
12
|
+
it "can resolve a 32 byte string without encoding" do
|
13
|
+
resolves_input("A"*16 + "B"*16)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "fails loudly when an invalid secret is provided" do
|
17
|
+
secret = Base64.urlsafe_encode64("bad")
|
18
|
+
expect do
|
19
|
+
AttrVault::Secret.new(secret)
|
20
|
+
end.to raise_error(AttrVault::InvalidSecret)
|
21
|
+
end
|
22
|
+
|
23
|
+
def resolves_input(input)
|
24
|
+
secret = AttrVault::Secret.new(input)
|
25
|
+
|
26
|
+
expect(
|
27
|
+
secret.signing_key
|
28
|
+
).to eq("A"*16)
|
29
|
+
|
30
|
+
expect(
|
31
|
+
secret.encryption_key
|
32
|
+
).to eq("B"*16)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,328 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
describe AttrVault do
|
5
|
+
context "with a single encrypted column" do
|
6
|
+
let(:key_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
|
7
|
+
let(:key_data) {
|
8
|
+
[{
|
9
|
+
id: key_id,
|
10
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
11
|
+
created_at: Time.now }].to_json
|
12
|
+
}
|
13
|
+
let(:item) {
|
14
|
+
# the let form can't be evaluated inside the class definition
|
15
|
+
# because Ruby scoping rules were written by H.P. Lovecraft, so
|
16
|
+
# we create a local here to work around that
|
17
|
+
k = key_data
|
18
|
+
Class.new(Sequel::Model(:items)) do
|
19
|
+
include AttrVault
|
20
|
+
vault_keyring k
|
21
|
+
vault_attr :secret
|
22
|
+
end
|
23
|
+
}
|
24
|
+
|
25
|
+
context "with a new object" do
|
26
|
+
it "does not affect other attributes" do
|
27
|
+
not_secret = 'jimi hendrix was rather talented'
|
28
|
+
s = item.create(not_secret: not_secret)
|
29
|
+
s.reload
|
30
|
+
expect(s.not_secret).to eq(not_secret)
|
31
|
+
expect(s.this.where(not_secret: not_secret).count).to eq 1
|
32
|
+
end
|
33
|
+
|
34
|
+
it "encrypts non-empty values" do
|
35
|
+
secret = 'lady gaga? also rather talented'
|
36
|
+
s = item.create(secret: secret)
|
37
|
+
s.reload
|
38
|
+
expect(s.secret).to eq(secret)
|
39
|
+
s.columns.each do |col|
|
40
|
+
expect(s.this.where(Sequel.cast(Sequel.cast(col, :text), :bytea) => secret).count).to eq 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it "stores empty values as empty" do
|
45
|
+
secret = ''
|
46
|
+
s = item.create(secret: secret)
|
47
|
+
s.reload
|
48
|
+
expect(s.secret).to eq('')
|
49
|
+
expect(s.secret_encrypted).to eq('')
|
50
|
+
end
|
51
|
+
|
52
|
+
it "stores nil values as nil" do
|
53
|
+
s = item.create(secret: nil)
|
54
|
+
s.reload
|
55
|
+
expect(s.secret).to be_nil
|
56
|
+
expect(s.secret_encrypted).to be_nil
|
57
|
+
end
|
58
|
+
|
59
|
+
it "stores the key id" do
|
60
|
+
secret = 'it was professor plum with the wrench in the library'
|
61
|
+
s = item.create(secret: secret)
|
62
|
+
s.reload
|
63
|
+
expect(s.key_id).to eq(key_id)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "with an existing object" do
|
68
|
+
it "does not affect other attributes" do
|
69
|
+
not_secret = 'soylent is not especially tasty'
|
70
|
+
s = item.create
|
71
|
+
s.update(not_secret: not_secret)
|
72
|
+
s.reload
|
73
|
+
expect(s.not_secret).to eq(not_secret)
|
74
|
+
expect(s.this.where(not_secret: not_secret).count).to eq 1
|
75
|
+
end
|
76
|
+
|
77
|
+
it "encrypts non-empty values" do
|
78
|
+
secret = 'soylent green is made of people'
|
79
|
+
s = item.create
|
80
|
+
s.update(secret: secret)
|
81
|
+
s.reload
|
82
|
+
expect(s.secret).to eq(secret)
|
83
|
+
s.columns.each do |col|
|
84
|
+
expect(s.this.where(Sequel.cast(Sequel.cast(col, :text), :bytea) => secret).count).to eq 0
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it "stores empty values as empty" do
|
89
|
+
s = item.create(secret: "darth vader is luke's father")
|
90
|
+
s.update(secret: '')
|
91
|
+
s.reload
|
92
|
+
expect(s.secret).to eq('')
|
93
|
+
expect(s.secret_encrypted).to eq('')
|
94
|
+
end
|
95
|
+
|
96
|
+
it "leaves nil values as nil" do
|
97
|
+
s = item.create(secret: "dr. crowe was dead all along")
|
98
|
+
s.update(secret: nil)
|
99
|
+
s.reload
|
100
|
+
expect(s.secret).to be_nil
|
101
|
+
expect(s.secret_encrypted).to be_nil
|
102
|
+
end
|
103
|
+
|
104
|
+
it "stores the key id" do
|
105
|
+
secret = 'animal style'
|
106
|
+
s = item.create
|
107
|
+
s.update(secret: secret)
|
108
|
+
s.reload
|
109
|
+
expect(s.key_id).to eq(key_id)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context "with multiple encrypted columns" do
|
115
|
+
let(:key_data) {
|
116
|
+
[{
|
117
|
+
id: '80a8571b-dc8a-44da-9b89-caee87e41ce2',
|
118
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
119
|
+
created_at: Time.now }].to_json
|
120
|
+
}
|
121
|
+
let(:item) {
|
122
|
+
k = key_data
|
123
|
+
Class.new(Sequel::Model(:items)) do
|
124
|
+
include AttrVault
|
125
|
+
vault_keyring k
|
126
|
+
vault_attr :secret
|
127
|
+
vault_attr :other
|
128
|
+
end
|
129
|
+
}
|
130
|
+
|
131
|
+
it "does not clobber other attributes" do
|
132
|
+
secret1 = "superman is really mild-mannered reporter clark kent"
|
133
|
+
secret2 = "batman is really millionaire playboy bruce wayne"
|
134
|
+
s = item.create(secret: secret1)
|
135
|
+
s.reload
|
136
|
+
expect(s.secret).to eq secret1
|
137
|
+
s.update(other: secret2)
|
138
|
+
s.reload
|
139
|
+
expect(s.secret).to eq secret1
|
140
|
+
expect(s.other).to eq secret2
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context "with items encrypted with an older key" do
|
145
|
+
let(:key1_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
|
146
|
+
let(:key1) {
|
147
|
+
{
|
148
|
+
id: key1_id,
|
149
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
150
|
+
created_at: Time.new(2014, 1, 1, 0, 0, 0)
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
let(:key2_id) { '0a85781b-d8ac-4a4d-89b9-acee874e1ec2' }
|
155
|
+
let(:key2) {
|
156
|
+
{
|
157
|
+
id: key2_id,
|
158
|
+
value: 'hUL1orBBRckZOuSuptRXYMV9lx5Qp54zwFUVwpwTpdk=',
|
159
|
+
created_at: Time.new(2014, 2, 1, 0, 0, 0)
|
160
|
+
}
|
161
|
+
}
|
162
|
+
let(:partial_keyring) {
|
163
|
+
[key1].to_json
|
164
|
+
}
|
165
|
+
|
166
|
+
let(:full_keyring) {
|
167
|
+
[key1, key2].to_json
|
168
|
+
}
|
169
|
+
let(:item1) {
|
170
|
+
k = partial_keyring
|
171
|
+
Class.new(Sequel::Model(:items)) do
|
172
|
+
include AttrVault
|
173
|
+
vault_keyring k
|
174
|
+
vault_attr :secret
|
175
|
+
vault_attr :other
|
176
|
+
end
|
177
|
+
}
|
178
|
+
let(:item2) {
|
179
|
+
k = full_keyring
|
180
|
+
Class.new(Sequel::Model(:items)) do
|
181
|
+
include AttrVault
|
182
|
+
vault_keyring k
|
183
|
+
vault_attr :secret
|
184
|
+
vault_attr :other
|
185
|
+
end
|
186
|
+
}
|
187
|
+
|
188
|
+
it "rewrites the items using the current key" do
|
189
|
+
secret1 = 'mrs. doubtfire is really a man'
|
190
|
+
secret2 = 'tootsie? also a man'
|
191
|
+
record = item1.create(secret: secret1)
|
192
|
+
expect(record.key_id).to eq key1_id
|
193
|
+
expect(record.secret).to eq secret1
|
194
|
+
|
195
|
+
old_secret_encrypted = record.secret_encrypted
|
196
|
+
old_secret_hmac = record.secret_hmac
|
197
|
+
|
198
|
+
new_key_record = item2[record.id]
|
199
|
+
new_key_record.update(secret: secret2)
|
200
|
+
new_key_record.reload
|
201
|
+
|
202
|
+
expect(new_key_record.key_id).to eq key2_id
|
203
|
+
expect(new_key_record.secret).to eq secret2
|
204
|
+
expect(new_key_record.secret_encrypted).not_to eq old_secret_encrypted
|
205
|
+
expect(new_key_record.secret_hmac).not_to eq old_secret_hmac
|
206
|
+
end
|
207
|
+
|
208
|
+
it "rewrites the items using the current key even if they are not updated" do
|
209
|
+
secret1 = 'the planet of the apes is really earth'
|
210
|
+
secret2 = 'the answer is 42'
|
211
|
+
record = item1.create(secret: secret1)
|
212
|
+
expect(record.key_id).to eq key1_id
|
213
|
+
expect(record.secret).to eq secret1
|
214
|
+
|
215
|
+
old_secret_encrypted = record.secret_encrypted
|
216
|
+
old_secret_hmac = record.secret_hmac
|
217
|
+
|
218
|
+
new_key_record = item2[record.id]
|
219
|
+
new_key_record.update(other: secret2)
|
220
|
+
new_key_record.reload
|
221
|
+
|
222
|
+
expect(new_key_record.key_id).to eq key2_id
|
223
|
+
expect(new_key_record.secret).to eq secret1
|
224
|
+
expect(new_key_record.secret_encrypted).not_to eq old_secret_encrypted
|
225
|
+
expect(new_key_record.secret_hmac).not_to eq old_secret_hmac
|
226
|
+
expect(new_key_record.other).to eq secret2
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
context "with plaintext source fields" do
|
231
|
+
let(:key_id) { '80a8571b-dc8a-44da-9b89-caee87e41ce2' }
|
232
|
+
let(:key_data) {
|
233
|
+
[{
|
234
|
+
id: key_id,
|
235
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
236
|
+
created_at: Time.now }].to_json
|
237
|
+
}
|
238
|
+
let(:item1) {
|
239
|
+
k = key_data
|
240
|
+
Class.new(Sequel::Model(:items)) do
|
241
|
+
include AttrVault
|
242
|
+
vault_keyring k
|
243
|
+
vault_attr :secret
|
244
|
+
vault_attr :other
|
245
|
+
end
|
246
|
+
}
|
247
|
+
let(:item2) {
|
248
|
+
k = key_data
|
249
|
+
Class.new(Sequel::Model(:items)) do
|
250
|
+
include AttrVault
|
251
|
+
vault_keyring k
|
252
|
+
vault_attr :secret, plaintext_source_field: :not_secret
|
253
|
+
vault_attr :other, plaintext_source_field: :other_not_secret
|
254
|
+
end
|
255
|
+
}
|
256
|
+
|
257
|
+
it "copies a plaintext field to an encrypted field when saving the object" do
|
258
|
+
becomes_secret = 'the location of the lost continent of atlantis'
|
259
|
+
s = item1.create(not_secret: becomes_secret)
|
260
|
+
reloaded = item2[s.id]
|
261
|
+
expect(reloaded.not_secret).to eq becomes_secret
|
262
|
+
reloaded.save
|
263
|
+
reloaded.reload
|
264
|
+
expect(reloaded.not_secret).to be_nil
|
265
|
+
expect(reloaded.secret).to eq becomes_secret
|
266
|
+
end
|
267
|
+
|
268
|
+
it "supports converting multiple fields" do
|
269
|
+
becomes_secret1 = 'the location of the fountain of youth'
|
270
|
+
becomes_secret2 = 'the location of the lost city of el dorado'
|
271
|
+
s = item1.create(not_secret: becomes_secret1, other_not_secret: becomes_secret2)
|
272
|
+
reloaded = item2[s.id]
|
273
|
+
expect(reloaded.not_secret).to eq becomes_secret1
|
274
|
+
expect(reloaded.other_not_secret).to eq becomes_secret2
|
275
|
+
reloaded.save
|
276
|
+
reloaded.reload
|
277
|
+
expect(reloaded.not_secret).to be_nil
|
278
|
+
expect(reloaded.secret).to eq becomes_secret1
|
279
|
+
expect(reloaded.other_not_secret).to be_nil
|
280
|
+
expect(reloaded.other).to eq becomes_secret2
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
context "with renamed database fields" do
|
285
|
+
let(:key_data) {
|
286
|
+
[{
|
287
|
+
id: '80a8571b-dc8a-44da-9b89-caee87e41ce2',
|
288
|
+
value: 'aFJDXs+798G7wgS/nap21LXIpm/Rrr39jIVo2m/cdj8=',
|
289
|
+
created_at: Time.now }].to_json
|
290
|
+
}
|
291
|
+
|
292
|
+
it "supports renaming the encrypted and hmac fields" do
|
293
|
+
k = key_data
|
294
|
+
item = Class.new(Sequel::Model(:items)) do
|
295
|
+
include AttrVault
|
296
|
+
vault_keyring k
|
297
|
+
vault_attr :classified_info,
|
298
|
+
encrypted_field: :secret_encrypted,
|
299
|
+
hmac_field: :secret_hmac
|
300
|
+
end
|
301
|
+
|
302
|
+
secret = "we've secretly replaced the fine coffee they usually serve with Folgers Crystals"
|
303
|
+
s = item.create(classified_info: secret)
|
304
|
+
s.reload
|
305
|
+
expect(s.classified_info).to eq secret
|
306
|
+
expect(s.secret_encrypted).not_to eq secret
|
307
|
+
expect(s.secret_hmac).not_to be_nil
|
308
|
+
end
|
309
|
+
|
310
|
+
it "supports renaming the key id field" do
|
311
|
+
k = key_data
|
312
|
+
item = Class.new(Sequel::Model(:items)) do
|
313
|
+
include AttrVault
|
314
|
+
vault_keyring k, key_field: :alt_key_id
|
315
|
+
vault_attr :secret
|
316
|
+
end
|
317
|
+
|
318
|
+
secret = "up up down down left right left right b a"
|
319
|
+
s = item.create(secret: secret)
|
320
|
+
s.reload
|
321
|
+
expect(s.secret).to eq secret
|
322
|
+
expect(s.secret_encrypted).not_to eq secret
|
323
|
+
expect(s.secret_hmac).not_to be_nil
|
324
|
+
expect(s.alt_key_id).not_to be_nil
|
325
|
+
expect(s.key_id).to be_nil
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
require 'bundler'
|
9
|
+
require 'attr_vault'
|
10
|
+
|
11
|
+
require 'pg'
|
12
|
+
require 'sequel'
|
13
|
+
|
14
|
+
conn = Sequel.connect(ENV['DATABASE_URL'])
|
15
|
+
conn.run 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'
|
16
|
+
conn.run 'DROP TABLE IF EXISTS items'
|
17
|
+
conn.run <<-EOF
|
18
|
+
CREATE TABLE items(
|
19
|
+
id serial primary key,
|
20
|
+
key_id uuid,
|
21
|
+
alt_key_id uuid,
|
22
|
+
secret_encrypted bytea,
|
23
|
+
secret_hmac bytea,
|
24
|
+
other_encrypted bytea,
|
25
|
+
other_hmac bytea,
|
26
|
+
not_secret text,
|
27
|
+
other_not_secret text
|
28
|
+
)
|
29
|
+
EOF
|
30
|
+
|
31
|
+
RSpec.configure do |config|
|
32
|
+
config.run_all_when_everything_filtered = true
|
33
|
+
config.filter_run :focus
|
34
|
+
|
35
|
+
config.before(:example) do
|
36
|
+
conn.run 'TRUNCATE items'
|
37
|
+
end
|
38
|
+
|
39
|
+
# Run specs in random order to surface order dependencies. If you find an
|
40
|
+
# order dependency and want to debug it, you can fix the order by providing
|
41
|
+
# the seed, which is printed after each run.
|
42
|
+
# --seed 1234
|
43
|
+
config.order = 'random'
|
44
|
+
|
45
|
+
config.expect_with :rspec do |c|
|
46
|
+
c.syntax = :expect
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attr_vault
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Maciek Sakrejda
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: fernet
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pg
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sequel
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.13'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.13'
|
69
|
+
description: Encryption at rest made easy
|
70
|
+
email:
|
71
|
+
- m.sakrejda@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- Gemfile
|
77
|
+
- Gemfile.lock
|
78
|
+
- LICENSE
|
79
|
+
- attr_vault.gemspec
|
80
|
+
- lib/attr_vault.rb
|
81
|
+
- lib/attr_vault/cryptor.rb
|
82
|
+
- lib/attr_vault/encryption.rb
|
83
|
+
- lib/attr_vault/errors.rb
|
84
|
+
- lib/attr_vault/keyring.rb
|
85
|
+
- lib/attr_vault/secret.rb
|
86
|
+
- lib/attr_vault/version.rb
|
87
|
+
- spec/attr_vault/keyring_spec.rb
|
88
|
+
- spec/attr_vault/secret_spec.rb
|
89
|
+
- spec/attr_vault_spec.rb
|
90
|
+
- spec/spec_helper.rb
|
91
|
+
homepage: https://github.com/deafbybeheading/attr_vault
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata: {}
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 2.2.2
|
112
|
+
signing_key:
|
113
|
+
specification_version: 4
|
114
|
+
summary: Sequel plugin for encryption at rest
|
115
|
+
test_files:
|
116
|
+
- spec/attr_vault/keyring_spec.rb
|
117
|
+
- spec/attr_vault/secret_spec.rb
|
118
|
+
- spec/attr_vault_spec.rb
|
119
|
+
- spec/spec_helper.rb
|
120
|
+
has_rdoc:
|