voynich 0.1.0
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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -0
- data/Gemfile +8 -0
- data/MIT-LICENSE +20 -0
- data/README.md +94 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/generators/voynich/active_record_generator.rb +18 -0
- data/lib/generators/voynich/model_attribute_generator.rb +33 -0
- data/lib/generators/voynich/templates/migration.rb +18 -0
- data/lib/voynich/active_model/model.rb +84 -0
- data/lib/voynich/active_record/models/data_key.rb +51 -0
- data/lib/voynich/active_record/models/value.rb +56 -0
- data/lib/voynich/active_record.rb +8 -0
- data/lib/voynich/aes.rb +51 -0
- data/lib/voynich/kms_data_key_client.rb +43 -0
- data/lib/voynich/storage.rb +34 -0
- data/lib/voynich/test_support.rb +17 -0
- data/lib/voynich/version.rb +3 -0
- data/lib/voynich.rb +52 -0
- data/voynich.gemspec +28 -0
- metadata +151 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 509ed50db8b90719408efd476aee02ea1fd76027
|
4
|
+
data.tar.gz: 06abb19e5e8d27374140e7d3fc8c9971f8f47d8f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b94f26b7e69829f516ad7c580addadb2e536ca785263dc094c3eac83174f25765ec0d4f177bbb4b679e5b2f7f08a4f654a4f9e7babbab95aaec760d7836993ce
|
7
|
+
data.tar.gz: 9d393b53a46256eb334d39a2dd23f45f7e9787770c664ba2497dfd8e5b143d47f138151ae7a60413b495d0f274b46ae47b6f8e35c10fce0771cb81f00a50057b
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
language: ruby
|
2
|
+
before_install: gem install bundler -v 1.10.6
|
3
|
+
sudo: false
|
4
|
+
cache:
|
5
|
+
bundler: true
|
6
|
+
rvm:
|
7
|
+
- 2.2
|
8
|
+
- 2.3.0
|
9
|
+
script:
|
10
|
+
- bundle exec rspec
|
11
|
+
notifications:
|
12
|
+
slack:
|
13
|
+
secure: R7BJ/vBOyKyqZrR999aArVourPQRixkA7CjUTl27hC4aZyO/XXwImcNovn+P+1d/R6mJ7yPBV4sTchga+AEhieNlOp/v2vBuQUi44li7GgIU5s/tgah+hgXpmgZE84cP0XcdubAMR7467YHlU/XvpCjtvVYFt1wAaVKGmftum1YP84GVOKnd4b1qFncJp4IIJDlMWOe8BxWWTkThWEhaYT+hGEO/mfGTNZiawHlsMFFedxZaXLqh2QVNeulfYYNhcyOdHDF0E8WBwvYOk/NyfW/prISPA64CJAAgglnQzXUdzhDJobbDcsl0c07J/7P+SDXhxzielPia0pV7IP+1htQs2OxKGdYuxF6hXr0VbWnBXP1vKP0yaxdaoI6mZG5Fm7m+D5IFbBlRvgZjd15IO3ThdykFbse9wRva5OSNcEB4HbqwsuqvojnqkgyJaHxQ29KucuFdHFfONdFZsCYezsNR9aPhA3jRbrGmY/vFY6mwcTmBi+p7+63xhPRXpuPTv+CKhoO0ZRN/w2avC2+jsFZDjR7o50QcFgPvYs6m0fENfvh03ubjMyKcS2y7A5gwpypzC9lkagkI8sE4Wna1iZnknHG5nBCkSPtd7/MGC7G0aiyikHlXf/4S/j0D9k9Pr0t+8ojnQWM0nfawoNpqBjwTpB561debYplpYlp/T4I=
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2016 Kazunori Kajihiro
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Voynich
|
2
|
+
|
3
|
+
Voynich is a secret storage library for Ruby on Rails backed by Amazon Key Management Service (KMS)
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'voynich', github: 'degica/voynich'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
### Generate Migration File
|
18
|
+
|
19
|
+
$ rails g voynich:active_record
|
20
|
+
$ rake db:migrate
|
21
|
+
|
22
|
+
## Configuration
|
23
|
+
|
24
|
+
Add this code to your initializer
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
Voynich.configure(
|
28
|
+
aws_access_key_id: 'aakid',
|
29
|
+
aws_secret_access_key: 'asak',
|
30
|
+
kms_cmk_id: 'cmk_id',
|
31
|
+
aws_region: 'us-east-1'
|
32
|
+
)
|
33
|
+
```
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
Voynich provides 2 types of interfaces.
|
38
|
+
|
39
|
+
### Storage interface
|
40
|
+
|
41
|
+
`Storage` provides generic accessors for encrypted attributes.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
## Create new encrypted data
|
45
|
+
### `create` method creates a new data key using KMS API and save the encrypted version of the key,
|
46
|
+
### then encrypt the plain value passed as an argument, save it, and return the UUID of the saved value
|
47
|
+
uuid = Voynich::Storage.new.create({credit_card: {number: "411111111111"}})
|
48
|
+
# => "131cd6e8-03da-48f7-bf99-672429c94e3f"
|
49
|
+
|
50
|
+
## Get decrypted data
|
51
|
+
### decrypting can be done by passing the UUID to `decrypt` method
|
52
|
+
data = Voynich::Storage.new.decrypt(uuid)
|
53
|
+
# => {credit_card: {number: "411111111111"}}
|
54
|
+
```
|
55
|
+
|
56
|
+
### ActiveModel integration
|
57
|
+
|
58
|
+
If you use Voynich with ActiveRecord models, you can use `Voynich::ActiveModel::Model` module to integrate your model with Voynich tables.
|
59
|
+
|
60
|
+
To use the module, run the following command. It will generate a migration file and add some lines to your model file.
|
61
|
+
|
62
|
+
$ rails g voynich:model_attribute YourModel model_attribute
|
63
|
+
|
64
|
+
Now the attribute is managed by Voynich
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
model = YourModel.new
|
68
|
+
# You can assign any type of data
|
69
|
+
model.secret_data = {card_number: '1234567890123456'}
|
70
|
+
|
71
|
+
# when the model is saved, encrypted data and key is created
|
72
|
+
model.save
|
73
|
+
|
74
|
+
# You can see the UUID of the voynich data is assigned
|
75
|
+
model.voynich_secret_data_value
|
76
|
+
# => #<Voynich::ActiveRecord::Value id: 1, data_key_id: 1, uuid: "...", ciphertext: "{\"c\":\"chD9hCWePs+Cqg...">
|
77
|
+
|
78
|
+
# You can get decrypted data just like a normal attribute
|
79
|
+
model.secret_data # => {card_number: '1234567890123456'}
|
80
|
+
```
|
81
|
+
|
82
|
+
## TODO
|
83
|
+
|
84
|
+
- [ ] Data key rotation
|
85
|
+
- [ ] Path based tree structure
|
86
|
+
- [ ] S3 adapter
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/degica/voynich.
|
91
|
+
|
92
|
+
## License
|
93
|
+
|
94
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "voynich"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "rails/generators/migration"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
|
4
|
+
module Voynich
|
5
|
+
class ActiveRecordGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
source_paths << File.join(File.dirname(__FILE__), "templates")
|
9
|
+
|
10
|
+
def create_migration_file
|
11
|
+
migration_template "migration.rb", "db/migrate/create_voynich_tables.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.next_migration_number(dirname)
|
15
|
+
::ActiveRecord::Generators::Base.next_migration_number dirname
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "rails/generators/migration"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
|
4
|
+
module Voynich
|
5
|
+
class ModelAttributeGenerator < Rails::Generators::Base
|
6
|
+
argument :model_class_name, type: :string
|
7
|
+
argument :attribute_name, type: :string
|
8
|
+
|
9
|
+
def include_module
|
10
|
+
inject_into_file(model_file_path, after: %r{class\s+#{model_class_name}\s+<\s+ActiveRecord::Base\n}) do <<-'RUBY'
|
11
|
+
include Voynich::ActiveModel::Model
|
12
|
+
RUBY
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_voynich_attribute
|
17
|
+
inject_into_file(model_file_path, after: "include Voynich::ActiveModel::Model\n",) do <<-RUBY
|
18
|
+
voynich_attribute :#{attribute_name}
|
19
|
+
RUBY
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate_migration
|
24
|
+
generate "migration", "AddVoynich#{attribute_name.classify}ValueIdTo#{model_class_name.pluralize} voynich_#{attribute_name}_value_id:integer"
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def model_file_path
|
30
|
+
File.join("app", "models", "#{model_class_name.underscore}.rb")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateVoynichTables < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table(:voynich_data_keys) do |t|
|
4
|
+
t.string :name, null: false
|
5
|
+
t.string :cmk_id, null: false
|
6
|
+
t.text :ciphertext, null: false
|
7
|
+
end
|
8
|
+
|
9
|
+
create_table(:voynich_values) do |t|
|
10
|
+
t.references :data_key, index: true, null: false
|
11
|
+
t.string :uuid, null: false
|
12
|
+
t.text :ciphertext, null: false
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :voynich_data_keys, :name, unique: true
|
16
|
+
add_index :voynich_values, :uuid, unique: true
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Voynich
|
4
|
+
module ActiveModel
|
5
|
+
module Model
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
delegate :voynich_targets, to: :class
|
10
|
+
delegate :voynich_column_name, to: :class
|
11
|
+
before_save :voynich_store_attributes
|
12
|
+
end
|
13
|
+
|
14
|
+
def voynich_context(name)
|
15
|
+
context_proc = voynich_targets[name.to_sym][:context]
|
16
|
+
if context_proc
|
17
|
+
context_proc.call(self)
|
18
|
+
else
|
19
|
+
{}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def voynich_store_attributes
|
24
|
+
voynich_targets.each do |name, options|
|
25
|
+
iv = instance_variable_get(:"@#{name}")
|
26
|
+
next if iv.nil?
|
27
|
+
|
28
|
+
column_name = voynich_column_name(name)
|
29
|
+
value = send(column_name) || send("build_#{column_name}")
|
30
|
+
value.context = voynich_context(name)
|
31
|
+
value.plain_value = iv
|
32
|
+
value.save!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
VOYNICH_DEFAULT_OPTIONS = {
|
37
|
+
column_prefix: 'voynich_',
|
38
|
+
column_suffix: '_value',
|
39
|
+
context: nil
|
40
|
+
}
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def voynich_targets
|
44
|
+
@voynich_targets ||= {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def voynich_column_name(name)
|
48
|
+
options = voynich_targets[name.to_sym]
|
49
|
+
"#{options[:column_prefix]}#{name}#{options[:column_suffix]}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def voynich_attribute(name, options = {})
|
53
|
+
options = VOYNICH_DEFAULT_OPTIONS.merge(options)
|
54
|
+
voynich_targets[name.to_sym] = options
|
55
|
+
asoc_options = options.
|
56
|
+
merge(class_name: "::Voynich::ActiveRecord::Value").
|
57
|
+
reject{|k| VOYNICH_DEFAULT_OPTIONS.keys.include? k}
|
58
|
+
|
59
|
+
belongs_to :"#{voynich_column_name(name)}", asoc_options
|
60
|
+
|
61
|
+
private :"#{voynich_column_name(name)}="
|
62
|
+
|
63
|
+
define_method(name) do
|
64
|
+
value = send(voynich_column_name(name))
|
65
|
+
iv = instance_variable_get(:"@#{name}")
|
66
|
+
return iv unless iv.nil?
|
67
|
+
return nil if value.nil?
|
68
|
+
value.context = voynich_context(name)
|
69
|
+
instance_variable_set(:"@#{name}", value.decrypt)
|
70
|
+
end
|
71
|
+
|
72
|
+
define_method("#{name}=") do |val|
|
73
|
+
instance_variable_set(:"@#{name}", val)
|
74
|
+
end
|
75
|
+
|
76
|
+
define_method("#{name}?") do
|
77
|
+
value = send(name)
|
78
|
+
value.respond_to?(:empty?) ? !value.empty? : !!value
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Voynich
|
2
|
+
module ActiveRecord
|
3
|
+
class DataKey < ::ActiveRecord::Base
|
4
|
+
self.table_name_prefix = 'voynich_'
|
5
|
+
|
6
|
+
attr_writer :plaintext
|
7
|
+
|
8
|
+
has_many :values, class_name: "Voynich::ActiveRecord::Value", dependent: :destroy
|
9
|
+
|
10
|
+
validates :name, presence: true, uniqueness: true
|
11
|
+
validates :cmk_id, presence: true
|
12
|
+
validates :ciphertext, presence: true
|
13
|
+
|
14
|
+
before_validation :generate_data_key, if: -> (m) { m.ciphertext.nil? }
|
15
|
+
|
16
|
+
def reencrypt!
|
17
|
+
result = client.reencrypt(ciphertext)
|
18
|
+
self.ciphertext = result.ciphertext
|
19
|
+
save!
|
20
|
+
end
|
21
|
+
|
22
|
+
def plaintext
|
23
|
+
return @plaintext unless @plaintext.nil?
|
24
|
+
if ciphertext.nil?
|
25
|
+
generate_data_key
|
26
|
+
else
|
27
|
+
decrypt_data_key
|
28
|
+
end
|
29
|
+
@plaintext
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def client
|
35
|
+
KMSDataKeyClient.new(cmk_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate_data_key
|
39
|
+
result = client.generate
|
40
|
+
self.ciphertext = result.ciphertext
|
41
|
+
self.plaintext = result.plaintext
|
42
|
+
end
|
43
|
+
|
44
|
+
def decrypt_data_key
|
45
|
+
result = client.decrypt(ciphertext)
|
46
|
+
self.ciphertext = result.ciphertext
|
47
|
+
self.plaintext = result.plaintext
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Voynich
|
4
|
+
module ActiveRecord
|
5
|
+
class Value < ::ActiveRecord::Base
|
6
|
+
self.table_name_prefix = 'voynich_'
|
7
|
+
|
8
|
+
attr_accessor :plain_value, :context
|
9
|
+
|
10
|
+
belongs_to :data_key, required: true, class_name: "Voynich::ActiveRecord::DataKey"
|
11
|
+
|
12
|
+
validates :uuid, presence: true, uniqueness: true
|
13
|
+
validates :ciphertext, presence: true
|
14
|
+
|
15
|
+
before_validation :generate_uuid, on: :create
|
16
|
+
before_validation :find_or_create_data_key
|
17
|
+
before_validation :encrypt
|
18
|
+
|
19
|
+
def decrypt
|
20
|
+
encrypted_data = JSON.parse(self.ciphertext, symbolize_names: true)
|
21
|
+
@plain_value = AES.new(data_key.plaintext, (context || {}).to_json).decrypt(
|
22
|
+
encrypted_data[:c],
|
23
|
+
iv: encrypted_data[:iv],
|
24
|
+
tag: encrypted_data[:t]
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def encrypt
|
29
|
+
return if plain_value.nil?
|
30
|
+
encrypted = AES.new(data_key.plaintext, (context || {}).to_json).encrypt(plain_value)
|
31
|
+
self.ciphertext = {
|
32
|
+
c: encrypted[:content],
|
33
|
+
t: encrypted[:tag],
|
34
|
+
iv: encrypted[:iv],
|
35
|
+
ad: encrypted[:auth_data]
|
36
|
+
}.to_json
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def find_or_create_data_key
|
42
|
+
if data_key.nil?
|
43
|
+
self.data_key = DataKey.find_or_create_by!(name: random_key_name, cmk_id: Voynich.kms_cmk_id)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def random_key_name
|
48
|
+
"auto:#{Random.rand(Voynich.auto_data_key_count)}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def generate_uuid
|
52
|
+
self.uuid ||= SecureRandom.uuid
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/voynich/aes.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Voynich
|
5
|
+
class AES
|
6
|
+
AUTH_TAG_BITS = 128
|
7
|
+
CIPHER_MODE = 'aes-256-gcm'
|
8
|
+
DEFAULT_SERIALIZER = Marshal
|
9
|
+
|
10
|
+
def initialize(secret, adata, serializer: DEFAULT_SERIALIZER)
|
11
|
+
@secret = secret
|
12
|
+
@auth_data = adata
|
13
|
+
@serializer = serializer
|
14
|
+
end
|
15
|
+
|
16
|
+
def encrypt(plaintext)
|
17
|
+
cipher = OpenSSL::Cipher.new(CIPHER_MODE)
|
18
|
+
cipher.encrypt
|
19
|
+
cipher.key = @secret
|
20
|
+
iv = cipher.random_iv
|
21
|
+
cipher.auth_data = @auth_data
|
22
|
+
encrypted_data = cipher.update(serialize(plaintext)) + cipher.final
|
23
|
+
tag = cipher.auth_tag(AUTH_TAG_BITS / 8)
|
24
|
+
{
|
25
|
+
content: Base64.strict_encode64(encrypted_data),
|
26
|
+
tag: Base64.strict_encode64(tag),
|
27
|
+
iv: Base64.strict_encode64(iv),
|
28
|
+
auth_data: @auth_data
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def decrypt(content, iv:, tag:)
|
33
|
+
cipher = OpenSSL::Cipher.new(CIPHER_MODE)
|
34
|
+
cipher.decrypt
|
35
|
+
cipher.key = @secret
|
36
|
+
cipher.iv = Base64.decode64(iv)
|
37
|
+
cipher.auth_tag = Base64.decode64(tag)
|
38
|
+
cipher.auth_data = @auth_data
|
39
|
+
decrypted_data = cipher.update(Base64.decode64(content)) + cipher.final
|
40
|
+
deserialize(decrypted_data)
|
41
|
+
end
|
42
|
+
|
43
|
+
def serialize(data)
|
44
|
+
@serializer.dump(data)
|
45
|
+
end
|
46
|
+
|
47
|
+
def deserialize(data)
|
48
|
+
@serializer.load(data)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Voynich
|
2
|
+
class KMSDataKeyClient
|
3
|
+
Result = Struct.new(:plaintext, :ciphertext)
|
4
|
+
|
5
|
+
attr_reader :cmk_id
|
6
|
+
|
7
|
+
def initialize(cmk_id)
|
8
|
+
@cmk_id = cmk_id
|
9
|
+
end
|
10
|
+
|
11
|
+
def decrypt(ciphertext)
|
12
|
+
response = kms_client.decrypt(ciphertext_blob: decode(ciphertext))
|
13
|
+
Result.new(encode(response.plaintext), ciphertext)
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate
|
17
|
+
response = kms_client.generate_data_key(key_id: cmk_id, key_spec: 'AES_256')
|
18
|
+
Result.new(encode(response.plaintext), encode(response.ciphertext_blob))
|
19
|
+
end
|
20
|
+
|
21
|
+
def reencrypt(ciphertext)
|
22
|
+
response = kms_client.re_encrypt(
|
23
|
+
ciphertext_blob: decode(ciphertext),
|
24
|
+
destination_key_id: cmk_id
|
25
|
+
)
|
26
|
+
Result.new(nil, encode(response.ciphertext_blob))
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def encode(data)
|
32
|
+
Base64.strict_encode64(data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def decode(data)
|
36
|
+
Base64.decode64(data)
|
37
|
+
end
|
38
|
+
|
39
|
+
def kms_client
|
40
|
+
@kms_client ||= Voynich.kms_client
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'voynich'
|
2
|
+
|
3
|
+
module Voynich
|
4
|
+
class Storage
|
5
|
+
def initialize
|
6
|
+
end
|
7
|
+
|
8
|
+
def create(plain_value, key_name: nil, context: {})
|
9
|
+
data_key = fetch_data_key(key_name) unless key_name.nil?
|
10
|
+
value = ActiveRecord::Value.create!(plain_value: plain_value, data_key: data_key, context: context)
|
11
|
+
value.uuid
|
12
|
+
end
|
13
|
+
|
14
|
+
def update(uuid, plain_value, context: {})
|
15
|
+
value = ActiveRecord::Value.find_by!(uuid: uuid)
|
16
|
+
value.plain_value = plain_value
|
17
|
+
value.context = context
|
18
|
+
value.save!
|
19
|
+
uuid
|
20
|
+
end
|
21
|
+
|
22
|
+
def decrypt(uuid, context: {})
|
23
|
+
value = ActiveRecord::Value.find_by!(uuid: uuid)
|
24
|
+
value.context = context
|
25
|
+
value.decrypt
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def fetch_data_key(key_name)
|
31
|
+
ActiveRecord::DataKey.find_or_create_by!(name: key_name, cmk_id: Voynich.kms_cmk_id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Voynich
|
2
|
+
module TestSupport
|
3
|
+
module StubKMS
|
4
|
+
def stub_kms_request
|
5
|
+
allow(Voynich).to receive(:kms_client) do
|
6
|
+
client = Aws::KMS::Client.new(stub_responses: true)
|
7
|
+
client.stub_responses(:generate_data_key,
|
8
|
+
plaintext: 'fourty length encoded plaintext data key',
|
9
|
+
ciphertext_blob: 'generated ciphertext blob')
|
10
|
+
client.stub_responses(:decrypt, plaintext: 'fourty length encoded plaintext data key')
|
11
|
+
client.stub_responses(:re_encrypt, ciphertext_blob: 'reencrypted ciphertext blob')
|
12
|
+
client
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/voynich.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require "voynich/version"
|
2
|
+
require "voynich/active_record"
|
3
|
+
require "voynich/kms_data_key_client"
|
4
|
+
require "voynich/active_model/model"
|
5
|
+
require "voynich/storage"
|
6
|
+
require "voynich/aes"
|
7
|
+
|
8
|
+
require 'aws-sdk'
|
9
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
10
|
+
|
11
|
+
module Voynich
|
12
|
+
mattr_accessor :kms_cmk_id
|
13
|
+
mattr_accessor :auto_data_key_count
|
14
|
+
mattr_accessor :aws_access_key_id
|
15
|
+
mattr_accessor :aws_secret_access_key
|
16
|
+
mattr_accessor :aws_region
|
17
|
+
|
18
|
+
DEFAULT_CONFIG = {
|
19
|
+
auto_data_key_max_count: 50,
|
20
|
+
aws_region: 'us-east-1'
|
21
|
+
}
|
22
|
+
|
23
|
+
def self.configure(config = {})
|
24
|
+
config = DEFAULT_CONFIG.merge(config)
|
25
|
+
self.kms_cmk_id = config[:kms_cmk_id]
|
26
|
+
self.auto_data_key_count = config[:auto_data_key_max_count]
|
27
|
+
self.aws_access_key_id = config[:aws_access_key_id]
|
28
|
+
self.aws_secret_access_key = config[:aws_secret_access_key]
|
29
|
+
self.aws_region = config[:aws_region]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.kms_client
|
33
|
+
if self.aws_access_key_id.present?
|
34
|
+
credentials = Aws::Credentials.new(self.aws_access_key_id, self.aws_secret_access_key)
|
35
|
+
Aws::KMS::Client.new(region: self.aws_region, credentials: credentials)
|
36
|
+
else
|
37
|
+
Aws::KMS::Client.new(region: self.aws_region)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Re-encrypts all existing data keys
|
42
|
+
# this should be executed when KMS CMK is rotated to
|
43
|
+
# have the data keys encrypted by the latest CMK
|
44
|
+
def reencrypt_all_data_keys
|
45
|
+
ActiveRecord::DataKey.find_each do |data_key|
|
46
|
+
data_key.reencrypt!
|
47
|
+
sleep 0.1 # KMS limits API access up to 100 calls/sec
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
self.configure
|
52
|
+
end
|
data/voynich.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'voynich/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "voynich"
|
8
|
+
spec.version = Voynich::VERSION
|
9
|
+
spec.authors = ["Kazunori Kajihiro"]
|
10
|
+
spec.email = ["kkajihiro@degica.com"]
|
11
|
+
|
12
|
+
spec.summary = "KMS backed secret management library"
|
13
|
+
spec.description = "KMS backed secret management library."
|
14
|
+
spec.homepage = "https://github.com/degica/voynich"
|
15
|
+
spec.licenses = ['MIT']
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.4"
|
25
|
+
spec.add_dependency "aws-sdk", "~> 2.2"
|
26
|
+
spec.add_dependency "activesupport", "~> 4.2"
|
27
|
+
spec.add_dependency "activerecord", "~> 4.2"
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: voynich
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kazunori Kajihiro
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.4'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: aws-sdk
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: activesupport
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4.2'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '4.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activerecord
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '4.2'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '4.2'
|
97
|
+
description: KMS backed secret management library.
|
98
|
+
email:
|
99
|
+
- kkajihiro@degica.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".travis.yml"
|
107
|
+
- Gemfile
|
108
|
+
- MIT-LICENSE
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- bin/console
|
112
|
+
- bin/setup
|
113
|
+
- lib/generators/voynich/active_record_generator.rb
|
114
|
+
- lib/generators/voynich/model_attribute_generator.rb
|
115
|
+
- lib/generators/voynich/templates/migration.rb
|
116
|
+
- lib/voynich.rb
|
117
|
+
- lib/voynich/active_model/model.rb
|
118
|
+
- lib/voynich/active_record.rb
|
119
|
+
- lib/voynich/active_record/models/data_key.rb
|
120
|
+
- lib/voynich/active_record/models/value.rb
|
121
|
+
- lib/voynich/aes.rb
|
122
|
+
- lib/voynich/kms_data_key_client.rb
|
123
|
+
- lib/voynich/storage.rb
|
124
|
+
- lib/voynich/test_support.rb
|
125
|
+
- lib/voynich/version.rb
|
126
|
+
- voynich.gemspec
|
127
|
+
homepage: https://github.com/degica/voynich
|
128
|
+
licenses:
|
129
|
+
- MIT
|
130
|
+
metadata: {}
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options: []
|
133
|
+
require_paths:
|
134
|
+
- lib
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
requirements: []
|
146
|
+
rubyforge_project:
|
147
|
+
rubygems_version: 2.5.1
|
148
|
+
signing_key:
|
149
|
+
specification_version: 4
|
150
|
+
summary: KMS backed secret management library
|
151
|
+
test_files: []
|