kms_encrypted 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11c711222f049df42c663c9e68a2822fb7e318d3f3fc02f6346801c998ead328
4
- data.tar.gz: 98200a87d8bf02a7398b075aaa6d79dff706a4d15d850584ebd952329980520b
3
+ metadata.gz: edd8de63fd58086fa749186d51d6a99cd4343600c8f50827fbb019f1ec2d9788
4
+ data.tar.gz: 7f91126392f0ad2e0a4c8d48d1a2f3c3fe7fc257a4eaef2bf526acf1badb491d
5
5
  SHA512:
6
- metadata.gz: 7b3d37f2739f1a869a8a34c62e9300c3b54b0e8b87b98377883fa252d161aaafcb387044049bf0bfb070e86d481034bcf5c19b0e83d21a4668a513b02f3da151
7
- data.tar.gz: b5596bc03466ac15b09d72cd1e00093eda05e2dcd3ca17607484ff678da808647bb7d364be27b5cfc912cdcccb991cbb55b18fd94206a4cb4510d17eaeb4c0da
6
+ metadata.gz: 7e83a606cd2b3cacf47854a7425245b163bcdd267a07a501d0344cad3fe98513fbafed99ed6096f53dade5aa8c36be267766293d7639525159c0be3b635e90c3
7
+ data.tar.gz: 9649c83297b1562e973f83cfcc1b67541f0e08ae2ef05cc6decaef661bd7b9f65b32ea3f35ac2faf8d6c7c10c1110e92e3760cdc8792c98734135abd4f3a882c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 1.0.1
2
+
3
+ - Added support for encryption and decryption outside models
4
+ - Added support for dynamic keys
5
+ - Fixed issue with inheritance
6
+
1
7
  ## 1.0.0
2
8
 
3
9
  - Added versioning
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2017-2018 Andrew Kane
1
+ Copyright (c) 2017-2019 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -5,13 +5,15 @@ Simple, secure key management for [attr_encrypted](https://github.com/attr-encry
5
5
  With KMS Encrypted:
6
6
 
7
7
  - Master encryption keys are not on application servers
8
+ - Encrypt and decrypt permissions can be granted separately
8
9
  - There’s an immutable audit log of all activity
9
10
  - Decryption can be disabled if an attack is detected
10
- - Encrypt and decrypt permissions can be granted separately
11
11
  - It’s easy to rotate keys
12
12
 
13
13
  Supports [AWS KMS](https://aws.amazon.com/kms/), [Google Cloud KMS](https://cloud.google.com/kms/), and [Vault](https://www.vaultproject.io/)
14
14
 
15
+ Check out [this post](https://ankane.org/sensitive-data-rails) for more info on securing sensitive data with Rails
16
+
15
17
  [![Build Status](https://travis-ci.org/ankane/kms_encrypted.svg?branch=master)](https://travis-ci.org/ankane/kms_encrypted)
16
18
 
17
19
  ## How It Works
@@ -30,6 +32,28 @@ Follow the instructions for your key management service:
30
32
  - [Google Cloud KMS](guides/Google.md)
31
33
  - [Vault](guides/Vault.md)
32
34
 
35
+ ## Outside Models
36
+
37
+ To encrypt and decrypt outside of models, create a box:
38
+
39
+ ```ruby
40
+ kms = KmsEncrypted::Box.new
41
+ ```
42
+
43
+ You can pass `key_id`, `version`, and `previous_versions` if needed.
44
+
45
+ Encrypt
46
+
47
+ ```ruby
48
+ kms.encrypt(message, context: {model_name: "User", model_id: 123})
49
+ ```
50
+
51
+ Decrypt
52
+
53
+ ```ruby
54
+ kms.decrypt(ciphertext, context: {model_name: "User", model_id: 123})
55
+ ```
56
+
33
57
  ## Related Projects
34
58
 
35
59
  To securely search encrypted data, check out [Blind Index](https://github.com/ankane/blind_index).
data/lib/kms_encrypted.rb CHANGED
@@ -5,6 +5,7 @@ require "json"
5
5
  require "securerandom"
6
6
 
7
7
  # modules
8
+ require "kms_encrypted/box"
8
9
  require "kms_encrypted/database"
9
10
  require "kms_encrypted/log_subscriber"
10
11
  require "kms_encrypted/model"
@@ -0,0 +1,91 @@
1
+ module KmsEncrypted
2
+ class Box
3
+ attr_reader :key_id, :version, :previous_versions
4
+
5
+ def initialize(key_id: nil, version: nil, previous_versions: nil)
6
+ @key_id = key_id || ENV["KMS_KEY_ID"]
7
+ @version = version || 1
8
+ @previous_versions = previous_versions || {}
9
+ end
10
+
11
+ def encrypt(plaintext, context: nil)
12
+ context = version_context(context, version)
13
+ key_id = version_key_id(version)
14
+ ciphertext = KmsEncrypted::Client.new(key_id: key_id, data_key: true).encrypt(plaintext, context: context)
15
+ "v#{version}:#{encode64(ciphertext)}"
16
+ end
17
+
18
+ def decrypt(ciphertext, context: nil)
19
+ m = /\Av(\d+):/.match(ciphertext)
20
+ if m
21
+ version = m[1].to_i
22
+ ciphertext = ciphertext.sub("v#{version}:", "")
23
+ else
24
+ version = 1
25
+ legacy_context = true
26
+
27
+ # legacy
28
+ if ciphertext.start_with?("$gc$")
29
+ _, _, short_key_id, ciphertext = ciphertext.split("$", 4)
30
+
31
+ # restore key, except for cryptoKeyVersion
32
+ stored_key_id = decode64(short_key_id).split("/")[0..3]
33
+ stored_key_id.insert(0, "projects")
34
+ stored_key_id.insert(2, "locations")
35
+ stored_key_id.insert(4, "keyRings")
36
+ stored_key_id.insert(6, "cryptoKeys")
37
+ key_id = stored_key_id.join("/")
38
+ elsif ciphertext.start_with?("vault:")
39
+ ciphertext = Base64.encode64(ciphertext)
40
+ end
41
+ end
42
+
43
+ key_id ||= version_key_id(version)
44
+ ciphertext = decode64(ciphertext)
45
+ context = version_context(context, version)
46
+
47
+ KmsEncrypted::Client.new(
48
+ key_id: key_id,
49
+ data_key: true,
50
+ legacy_context: legacy_context
51
+ ).decrypt(ciphertext, context: context)
52
+ end
53
+
54
+ private
55
+
56
+ def version_key_id(version)
57
+ key_id =
58
+ if previous_versions[version]
59
+ previous_versions[version][:key_id]
60
+ elsif self.version == version
61
+ self.key_id
62
+ else
63
+ raise KmsEncrypted::Error, "Version not active: #{version}"
64
+ end
65
+
66
+ raise ArgumentError, "Missing key id" unless key_id
67
+
68
+ key_id
69
+ end
70
+
71
+ def version_context(context, version)
72
+ if context.respond_to?(:call)
73
+ if context.arity == 0
74
+ context.call
75
+ else
76
+ context.call(version)
77
+ end
78
+ else
79
+ context
80
+ end
81
+ end
82
+
83
+ def encode64(bytes)
84
+ Base64.strict_encode64(bytes)
85
+ end
86
+
87
+ def decode64(bytes)
88
+ Base64.decode64(bytes)
89
+ end
90
+ end
91
+ end
@@ -8,32 +8,20 @@ module KmsEncrypted
8
8
  @options = record.class.kms_keys[key_method.to_sym]
9
9
  end
10
10
 
11
- def name
12
- options[:name]
11
+ def version
12
+ @version ||= evaluate_option(:version).to_i
13
13
  end
14
14
 
15
- def current_version
16
- @version ||= begin
17
- version = options[:version]
18
- version = record.instance_exec(&version) if version.respond_to?(:call)
19
- version.to_i
20
- end
15
+ def key_id
16
+ @key_id ||= evaluate_option(:key_id)
21
17
  end
22
18
 
23
- def key_version(version)
24
- versions = (options[:previous_versions] || {}).dup
25
- versions[current_version] ||= options.slice(:key_id)
26
-
27
- raise KmsEncrypted::Error, "Version not active: #{version}" unless versions[version]
28
-
29
- key_id = versions[version][:key_id]
30
-
31
- raise ArgumentError, "Missing key id" unless key_id
32
-
33
- key_id
19
+ def previous_versions
20
+ @previous_versions ||= evaluate_option(:previous_versions)
34
21
  end
35
22
 
36
23
  def context(version)
24
+ name = options[:name]
37
25
  context_method = name ? "kms_encryption_context_#{name}" : "kms_encryption_context"
38
26
  if record.method(context_method).arity == 0
39
27
  record.send(context_method)
@@ -43,51 +31,33 @@ module KmsEncrypted
43
31
  end
44
32
 
45
33
  def encrypt(plaintext)
46
- key_id = key_version(current_version)
47
- context = context(current_version)
48
- ciphertext = KmsEncrypted::Client.new(key_id: key_id, data_key: true).encrypt(plaintext, context: context)
49
- "v#{current_version}:#{encode64(ciphertext)}"
34
+ context = context(version)
35
+
36
+ KmsEncrypted::Box.new(
37
+ key_id: key_id,
38
+ version: version,
39
+ previous_versions: previous_versions
40
+ ).encrypt(plaintext, context: context)
50
41
  end
51
42
 
52
43
  def decrypt(ciphertext)
44
+ # determine version for context
53
45
  m = /\Av(\d+):/.match(ciphertext)
54
- if m
55
- version = m[1].to_i
56
- ciphertext = ciphertext.sub("v#{version}:", "")
57
- else
58
- version = 1
59
- context = {} if options[:upgrade_context]
60
- legacy_context = true
61
-
62
- # legacy
63
- if ciphertext.start_with?("$gc$")
64
- _, _, short_key_id, ciphertext = ciphertext.split("$", 4)
65
-
66
- # restore key, except for cryptoKeyVersion
67
- stored_key_id = decode64(short_key_id).split("/")[0..3]
68
- stored_key_id.insert(0, "projects")
69
- stored_key_id.insert(2, "locations")
70
- stored_key_id.insert(4, "keyRings")
71
- stored_key_id.insert(6, "cryptoKeys")
72
- key_id = stored_key_id.join("/")
73
- elsif ciphertext.start_with?("vault:")
74
- ciphertext = Base64.encode64(ciphertext)
75
- end
76
- end
77
-
78
- key_id ||= key_version(version)
79
- context ||= context(version)
80
- ciphertext = decode64(ciphertext)
46
+ version = m ? m[1].to_i : 1
47
+ context = (options[:upgrade_context] && !m) ? {} : context(version)
81
48
 
82
- KmsEncrypted::Client.new(key_id: key_id, data_key: true, legacy_context: legacy_context).decrypt(ciphertext, context: context)
49
+ KmsEncrypted::Box.new(
50
+ key_id: key_id,
51
+ previous_versions: previous_versions
52
+ ).decrypt(ciphertext, context: context)
83
53
  end
84
54
 
85
- def encode64(bytes)
86
- Base64.strict_encode64(bytes)
87
- end
55
+ private
88
56
 
89
- def decode64(bytes)
90
- Base64.decode64(bytes)
57
+ def evaluate_option(key)
58
+ opt = options[key]
59
+ opt = record.instance_exec(&opt) if opt.respond_to?(:call)
60
+ opt
91
61
  end
92
62
  end
93
63
  end
@@ -8,12 +8,22 @@ module KmsEncrypted
8
8
  context_method = name ? "kms_encryption_context_#{name}" : "kms_encryption_context"
9
9
 
10
10
  class_eval do
11
- class << self
12
- def kms_keys
13
- @kms_keys ||= {}
14
- end unless respond_to?(:kms_keys)
11
+ @kms_keys ||= {}
12
+
13
+ unless respond_to?(:kms_keys)
14
+ def self.kms_keys
15
+ parent_keys =
16
+ if superclass.respond_to?(:kms_keys)
17
+ superclass.kms_keys
18
+ else
19
+ {}
20
+ end
21
+
22
+ parent_keys.merge(@kms_keys || {})
23
+ end
15
24
  end
16
- kms_keys[key_method.to_sym] = {
25
+
26
+ @kms_keys[key_method.to_sym] = {
17
27
  key_id: key_id,
18
28
  name: name,
19
29
  version: version,
@@ -21,7 +31,7 @@ module KmsEncrypted
21
31
  upgrade_context: upgrade_context
22
32
  }
23
33
 
24
- if kms_keys.size == 1
34
+ if @kms_keys.size == 1
25
35
  after_save :encrypt_kms_keys
26
36
 
27
37
  # fetch all keys together so only need to update database once
@@ -1,3 +1,3 @@
1
1
  module KmsEncrypted
2
- VERSION = "1.0.0"
2
+ VERSION = "1.0.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kms_encrypted
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-17 00:00:00.000000000 Z
11
+ date: 2019-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '4.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '4.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -160,6 +160,7 @@ files:
160
160
  - LICENSE.txt
161
161
  - README.md
162
162
  - lib/kms_encrypted.rb
163
+ - lib/kms_encrypted/box.rb
163
164
  - lib/kms_encrypted/client.rb
164
165
  - lib/kms_encrypted/clients/aws.rb
165
166
  - lib/kms_encrypted/clients/base.rb