enveloperb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ Ruby bindings for the [envelopers](https://github.com/cipherstash/enveloper) envelope encryption library.
2
+
3
+ Envelope encryption is a mechanism by which a plaintext is encrypted into a ciphertext using a single-use key (known as the "data key"), and then that data key is encrypted with a second key (known as the "wrapping key", or "key-encryption key", or sometimes "KEK").
4
+ The encrypted data key is then stored alongside the ciphertext, so that all that is needed for decryption is the key-encryption key and the ciphertext/encrypted data key bundle.
5
+
6
+ The benefits of this mechanism are:
7
+
8
+ 1. Compromise of the key used to encrypt a plaintext (say, by short-term penetration of a process performing decryption) does not compromise all data;
9
+
10
+ 2. The key-encryption key can be stored securely and entirely separate from any plaintext data, in an HSM (Hardware Security Module) or other hardened environment;
11
+
12
+ 3. The entity operating the key-encryption key environment never has (direct) access to plaintexts (as would be the case if you sent the plaintext to the HSM for encryption);
13
+
14
+ 4. Large volumes of data can be encrypted efficiently on a local machine, and only the small data key needs to be sent over a slow network link to be encrypted.
15
+
16
+ As you can see, the benefits of envelope encryption mostly center around environments where KEK material is HSM-managed.
17
+ Except for testing purposes, it is not common to use envelope encryption in situations where the KEK is provided directly to the envelope encryption system.
18
+
19
+
20
+ # Installation
21
+
22
+ In order to build the `enveloperb` gem, you must have Rust 1.31.0 or later installed.
23
+ On an ARM-based platform, you must use Rust nightly, for SIMD intrinsics support.
24
+
25
+ With that available, you should be able to install it like any other gem:
26
+
27
+ gem install enveloperb
28
+
29
+ There's also the wonders of [the Gemfile](http://bundler.io):
30
+
31
+ gem 'enveloperb'
32
+
33
+ If you're the sturdy type that likes to run from git:
34
+
35
+ bundle install
36
+ rake install
37
+
38
+ Or, if you've eschewed the convenience of Rubygems entirely, then you
39
+ presumably know what to do already.
40
+
41
+
42
+ # Usage
43
+
44
+ First off, load the library:
45
+
46
+ ```ruby
47
+ require "enveloperb"
48
+ ```
49
+
50
+ Then create a new cryptography engine, using your choice of wrapping key provider.
51
+ For this example, we'll use the "simple" key provider, which takes a 16 byte *binary* string as the key-encryption-key.
52
+
53
+ ```ruby
54
+ require "securerandom"
55
+ kek = SecureRandom.bytes(16)
56
+
57
+ engine = Enveloperb::Simple.new(kek)
58
+ ```
59
+
60
+ Now you can encrypt whatever data you like:
61
+
62
+ ```ruby
63
+ ct = engine.encrypt("This is a super-important secret")
64
+ ```
65
+
66
+ This produces an `Enveloperb::EncryptedRecord`, which can be turned into a (binary) string very easily:
67
+
68
+ ```ruby
69
+ File.binwrite("/tmp/ciphertext", ct1.to_s)
70
+ ```
71
+
72
+ To turn a binary string back into a ciphertext, just create a new `EncryptedRecord` with it:
73
+
74
+ ```ruby
75
+ ct_new = Enveloperb::EncryptedRecord.new(File.binread("/tmp/ciphertext"))
76
+ ```
77
+
78
+ Then you can decrypt it again:
79
+
80
+ ```ruby
81
+ engine.decrypt(ct_new) # => "This ia super-important secret"
82
+ ```
83
+
84
+
85
+ ## AWS KMS Key Provider
86
+
87
+ When using a locally-managed wrapping key, the benefits over direct encryption aren't significant.
88
+ The real benefits come when using a secured key provider for the wrapping key, such as AWS KMS.
89
+
90
+ To use an AWS KMS key as the wrapping key, you use an `Enveloperb::AWSKMS` instance as the cryptography engine, like so:
91
+
92
+ ```ruby
93
+ engine = Enveloperb::AWSKMS.key(keyid, profile: "example", region: "xx-example-1", credentials: { ... })
94
+ ```
95
+
96
+ While `keyid` is mandatory, `profile`, `region` and `credentials` are all optional.
97
+ If not specified, they will be extracted from the usual places (environment, metadata service, etc) as specified in [the AWS SDK for Rust documentation](https://docs.aws.amazon.com/sdk-for-rust/latest/dg/credentials.html).
98
+ Yes, the Rust SDK -- `enveloperb` is just a thin wrapper around a Rust library.
99
+ We are truly living in the future.
100
+
101
+ Once you have your AWS KMS cryptography engine, its usage is the familiar `#encrypt` / `#decrypt` cycle.
102
+
103
+
104
+ # Contributing
105
+
106
+ Please see [CONTRIBUTING.md](CONTRIBUTING.md).
107
+
108
+
109
+ # Licence
110
+
111
+ Unless otherwise stated, everything in this repo is covered by the following
112
+ copyright notice:
113
+
114
+ Copyright (C) 2022 CipherStash Inc.
115
+
116
+ This program is free software: you can redistribute it and/or modify it
117
+ under the terms of the GNU General Public License version 3, as
118
+ published by the Free Software Foundation.
119
+
120
+ This program is distributed in the hope that it will be useful,
121
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
122
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
123
+ GNU General Public License for more details.
124
+
125
+ You should have received a copy of the GNU General Public License
126
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1,40 @@
1
+ begin
2
+ require 'git-version-bump'
3
+ rescue LoadError
4
+ nil
5
+ end
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "enveloperb"
9
+
10
+ s.version = GVB.version rescue "0.0.0.1.NOGVB"
11
+ s.date = GVB.date rescue Time.now.strftime("%Y-%m-%d")
12
+
13
+ s.platform = Gem::Platform::RUBY
14
+
15
+ s.summary = "Ruby bindings for the envelopers envelope encryption library"
16
+
17
+ s.authors = ["Matt Palmer"]
18
+ s.email = ["matt@cipherstash.com"]
19
+ s.homepage = "https://github.com/cipherstash/enveloperb"
20
+
21
+ s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(\.|G|spec|Rakefile)/ }
22
+
23
+ s.extensions = ["ext/Rakefile"]
24
+ s.require_paths = ["lib", "target"]
25
+
26
+ s.required_ruby_version = ">= 2.7.0"
27
+
28
+ s.add_runtime_dependency "rutie", "~> 0.0.4"
29
+
30
+ s.add_development_dependency 'bundler'
31
+ s.add_development_dependency 'gem-compiler'
32
+ s.add_development_dependency 'github-release'
33
+ s.add_development_dependency 'guard-rspec'
34
+ s.add_development_dependency 'rake', '~> 10.4', '>= 10.4.2'
35
+ s.add_development_dependency 'rb-inotify', '~> 0.9'
36
+ s.add_development_dependency 'redcarpet'
37
+ s.add_development_dependency 'rspec'
38
+ s.add_development_dependency 'simplecov'
39
+ s.add_development_dependency 'yard'
40
+ end
data/ext/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ task :build do
2
+ sh "cargo build --release"
3
+ end
4
+
5
+ task default: :build
@@ -0,0 +1,58 @@
1
+ module Enveloperb
2
+ # An Enveloperb cryptography engine using AWS KMS as a wrapping key provider.
3
+ #
4
+ class AWSKMS
5
+ def self.new(keyid, aws_access_key_id: nil, aws_secret_access_key: nil, aws_session_token: nil, aws_region: nil)
6
+ unless keyid.is_a?(String) && keyid.encoding == Encoding::find("UTF-8") && keyid.valid_encoding?
7
+ raise ArgumentError, "Key ID must be a valid UTF-8 string"
8
+ end
9
+
10
+ unless aws_access_key_id.nil? && aws_secret_access_key.nil? && aws_session_token.nil? && aws_region.nil?
11
+ validate_string(aws_access_key_id, :aws_access_key_id)
12
+ validate_string(aws_secret_access_key, :aws_secret_access_key)
13
+ validate_string(aws_region, :aws_region)
14
+ validate_string(aws_session_token, :aws_session_token, allow_nil: true)
15
+ end
16
+
17
+ _new(
18
+ keyid,
19
+ {
20
+ access_key_id: aws_access_key_id,
21
+ secret_access_key: aws_secret_access_key,
22
+ session_token: aws_session_token,
23
+ region: aws_region,
24
+ }
25
+ )
26
+ end
27
+
28
+ def encrypt(s)
29
+ unless s.is_a?(String)
30
+ raise ArgumentError, "Can only encrypt strings"
31
+ end
32
+
33
+ _encrypt(s)
34
+ end
35
+
36
+ def decrypt(er)
37
+ unless er.is_a?(EncryptedRecord)
38
+ raise ArgumentError, "Can only decrypt EncryptedRecord objects; you can make one from a string with EncryptedRecord.new"
39
+ end
40
+
41
+ _decrypt(er)
42
+ end
43
+
44
+ class << self
45
+ private
46
+
47
+ def validate_string(s, var, allow_nil: false)
48
+ if s.nil? && !allow_nil
49
+ raise ArgumentError, "#{var.inspect} option to Enveloperb::AWSKMS.new() cannot be nil"
50
+ end
51
+
52
+ unless s.is_a?(String) && s.encoding == Encoding.find("UTF-8") && s.valid_encoding?
53
+ raise ArgumentError, "#{var.inspect} option passed to Enveloperb::AWSKMS.new() must be a valid UTF-8 string"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,31 @@
1
+ module Enveloperb
2
+ # An envelope encrypted record.
3
+ class EncryptedRecord
4
+ # Create an encrypted record from a serialized form.
5
+ #
6
+ # Encrypted records can be serialized (using #to_s), and then deserialized by passing them into this constructor.
7
+ #
8
+ # @param s [String] the serialized encrypted record.
9
+ # This must be a `BINARY` encoded string.
10
+ #
11
+ # @raises [ArgumentError] if something other than a binary string is provided, or if the string passed as the serialized encrypted record is not valid.
12
+ #
13
+ def self.new(s)
14
+ unless s.is_a?(String) && s.encoding == Encoding::BINARY
15
+ raise ArgumentError, "Serialized encrypted record must be a binary string"
16
+ end
17
+
18
+ _new(s)
19
+ end
20
+
21
+ # Serialize an encrypted record into a string.
22
+ #
23
+ # @return [String]
24
+ #
25
+ # @raise [RuntimeError] if something goes spectacularly wrong with the serialization process.
26
+ #
27
+ def to_s
28
+ _serialize
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ module Enveloperb
2
+ # An Enveloperb cryptography engine using an unprotected string as the wrapping key.
3
+ #
4
+ # @note this class is not intended for general-purpose use.
5
+ #
6
+ class Simple
7
+ def self.new(k)
8
+ unless k.is_a?(String) && k.encoding == Encoding::BINARY
9
+ raise ArgumentError, "Key must be a binary string"
10
+ end
11
+
12
+ unless k.bytesize == 16
13
+ raise ArgumentError, "Key must be 16 bytes"
14
+ end
15
+
16
+ _new(k)
17
+ end
18
+
19
+ def encrypt(s)
20
+ unless s.is_a?(String)
21
+ raise ArgumentError, "Can only encrypt strings"
22
+ end
23
+
24
+ _encrypt(s)
25
+ end
26
+
27
+ def decrypt(er)
28
+ unless er.is_a?(EncryptedRecord)
29
+ raise ArgumentError, "Can only decrypt EncryptedRecord objects; you can make one from a string with EncryptedRecord.new"
30
+ end
31
+
32
+ _decrypt(er)
33
+ end
34
+ end
35
+ end
data/lib/enveloperb.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "rutie"
2
+
3
+ module Enveloperb
4
+ Rutie.new(:enveloperb).init 'Init_enveloperb', __dir__
5
+ end
6
+
7
+ require_relative "./enveloperb/encrypted_record"
8
+ require_relative "./enveloperb/awskms"
9
+ require_relative "./enveloperb/simple"
data/src/lib.rs ADDED
@@ -0,0 +1,195 @@
1
+ #[macro_use]
2
+ extern crate rutie;
3
+
4
+ #[macro_use]
5
+ extern crate lazy_static;
6
+
7
+ // I have deliberately not used more-specific symbols inside the aws_* crates because the
8
+ // names are quite generic, and in the near future when we have more providers, we'll
9
+ // almost certainly end up with naming clashes.
10
+ use aws_config;
11
+ use aws_sdk_kms;
12
+ use aws_types;
13
+ use enveloper::{EncryptedRecord, EnvelopeCipher, KMSKeyProvider, SimpleKeyProvider};
14
+ use std::borrow::Cow;
15
+ use tokio::runtime::Runtime;
16
+ use rutie::{Class, Encoding, Hash, Module, Object, RString, Symbol, VerifiedObject, VM};
17
+
18
+ module!(Enveloperb);
19
+ class!(EnveloperbSimple);
20
+ class!(EnveloperbAWSKMS);
21
+ class!(EnveloperbEncryptedRecord);
22
+
23
+ impl VerifiedObject for EnveloperbEncryptedRecord {
24
+ fn is_correct_type<T: Object>(object: &T) -> bool {
25
+ let klass = Module::from_existing("Enveloperb").get_nested_class("EncryptedRecord");
26
+ klass.case_equals(object)
27
+ }
28
+
29
+ fn error_message() -> &'static str {
30
+ "Error converting to Enveloperb::EncryptedRecord"
31
+ }
32
+ }
33
+
34
+ pub struct SimpleCipher {
35
+ cipher: EnvelopeCipher<SimpleKeyProvider>,
36
+ runtime: Runtime,
37
+ }
38
+
39
+ pub struct AWSKMSCipher {
40
+ cipher: EnvelopeCipher<KMSKeyProvider>,
41
+ runtime: Runtime,
42
+ }
43
+
44
+ wrappable_struct!(SimpleCipher, SimpleCipherWrapper, SIMPLE_CIPHER_WRAPPER);
45
+ wrappable_struct!(AWSKMSCipher, AWSKMSCipherWrapper, AWSKMS_CIPHER_WRAPPER);
46
+ wrappable_struct!(EncryptedRecord, EncryptedRecordWrapper, ENCRYPTED_RECORD_WRAPPER);
47
+
48
+ methods!(
49
+ EnveloperbSimple,
50
+ rbself,
51
+
52
+ fn enveloperb_simple_new(rbkey: RString) -> EnveloperbSimple {
53
+ let mut key: [u8; 16] = Default::default();
54
+
55
+ key.clone_from_slice(rbkey.unwrap().to_bytes_unchecked());
56
+
57
+ let provider = SimpleKeyProvider::init(key);
58
+ let cipher = EnvelopeCipher::init(provider);
59
+
60
+ let klass = Module::from_existing("Enveloperb").get_nested_class("Simple");
61
+ return klass.wrap_data(SimpleCipher{ cipher: cipher, runtime: Runtime::new().unwrap() }, &*SIMPLE_CIPHER_WRAPPER);
62
+ }
63
+
64
+ fn enveloperb_simple_encrypt(rbtext: RString) -> EnveloperbEncryptedRecord {
65
+ let cipher = rbself.get_data(&*SIMPLE_CIPHER_WRAPPER);
66
+ let er_r = cipher.runtime.block_on(async {
67
+ cipher.cipher.encrypt(rbtext.unwrap().to_bytes_unchecked()).await
68
+ });
69
+ let er = er_r.map_err(|e| VM::raise(Class::from_existing("RuntimeError"), &format!("Failed to perform encryption: {:?}", e))).unwrap();
70
+
71
+ let klass = Module::from_existing("Enveloperb").get_nested_class("EncryptedRecord");
72
+ return klass.wrap_data(er, &*ENCRYPTED_RECORD_WRAPPER);
73
+ }
74
+
75
+ fn enveloperb_simple_decrypt(rbrecord: EnveloperbEncryptedRecord) -> RString {
76
+ let cipher = rbself.get_data(&*SIMPLE_CIPHER_WRAPPER);
77
+ let e_record = rbrecord.unwrap();
78
+ let record = e_record.get_data(&*ENCRYPTED_RECORD_WRAPPER);
79
+
80
+ let vec_r = cipher.runtime.block_on(async {
81
+ cipher.cipher.decrypt(record).await
82
+ });
83
+ let vec = vec_r.map_err(|e| VM::raise(Class::from_existing("RuntimeError"), &format!("Failed to perform decryption: {:?}", e))).unwrap();
84
+
85
+ return RString::from_bytes(&vec, &Encoding::find("BINARY").unwrap());
86
+ }
87
+ );
88
+
89
+ methods!(
90
+ EnveloperbAWSKMS,
91
+ rbself,
92
+
93
+ fn enveloperb_awskms_new(rbkey: RString, rbcreds: Hash) -> EnveloperbAWSKMS {
94
+ let raw_creds = rbcreds.unwrap();
95
+ let rt = Runtime::new().unwrap();
96
+
97
+ let kmsclient_config = if raw_creds.at(&Symbol::new("access_key_id")).is_nil() {
98
+ rt.block_on(async {
99
+ aws_config::load_from_env().await
100
+ })
101
+ } else {
102
+ let rbregion = raw_creds.at(&Symbol::new("region")).try_convert_to::<RString>().unwrap();
103
+ let region = Some(aws_types::region::Region::new(rbregion.to_str().to_owned()));
104
+
105
+ let rbkey_id = raw_creds.at(&Symbol::new("access_key_id")).try_convert_to::<RString>().unwrap();
106
+ let key_id = rbkey_id.to_str();
107
+
108
+ let rbsecret = raw_creds.at(&Symbol::new("secret_access_key")).try_convert_to::<RString>().unwrap();
109
+ let secret = rbsecret.to_str();
110
+
111
+ let token = match raw_creds.at(&Symbol::new("session_token")).try_convert_to::<RString>() {
112
+ Ok(str) => Some(str.to_string()),
113
+ Err(_) => None
114
+ };
115
+
116
+ let aws_creds = aws_types::Credentials::from_keys(key_id, secret, token);
117
+ let creds_provider = aws_types::credentials::SharedCredentialsProvider::new(aws_creds);
118
+
119
+ aws_types::sdk_config::SdkConfig::builder().region(region).credentials_provider(creds_provider).build()
120
+ };
121
+
122
+ let kmsclient = aws_sdk_kms::Client::new(&kmsclient_config);
123
+ let provider = KMSKeyProvider::new(kmsclient, rbkey.unwrap().to_string());
124
+ let cipher = EnvelopeCipher::init(provider);
125
+
126
+ let klass = Module::from_existing("Enveloperb").get_nested_class("AWSKMS");
127
+ return klass.wrap_data(AWSKMSCipher{ cipher: cipher, runtime: rt }, &*AWSKMS_CIPHER_WRAPPER);
128
+ }
129
+
130
+ fn enveloperb_awskms_encrypt(rbtext: RString) -> EnveloperbEncryptedRecord {
131
+ let cipher = rbself.get_data(&*AWSKMS_CIPHER_WRAPPER);
132
+ let er_r = cipher.runtime.block_on(async {
133
+ cipher.cipher.encrypt(rbtext.unwrap().to_bytes_unchecked()).await
134
+ });
135
+ let er = er_r.map_err(|e| VM::raise(Class::from_existing("RuntimeError"), &format!("Failed to perform encryption: {:?}", e))).unwrap();
136
+
137
+ let klass = Module::from_existing("Enveloperb").get_nested_class("EncryptedRecord");
138
+ return klass.wrap_data(er, &*ENCRYPTED_RECORD_WRAPPER);
139
+ }
140
+
141
+ fn enveloperb_awskms_decrypt(rbrecord: EnveloperbEncryptedRecord) -> RString {
142
+ let cipher = rbself.get_data(&*AWSKMS_CIPHER_WRAPPER);
143
+ let e_record = rbrecord.unwrap();
144
+ let record = e_record.get_data(&*ENCRYPTED_RECORD_WRAPPER);
145
+
146
+ let vec_r = cipher.runtime.block_on(async {
147
+ cipher.cipher.decrypt(record).await
148
+ });
149
+ let vec = vec_r.map_err(|e| VM::raise(Class::from_existing("RuntimeError"), &format!("Failed to perform decryption: {:?}", e))).unwrap();
150
+
151
+ return RString::from_bytes(&vec, &Encoding::find("BINARY").unwrap());
152
+ }
153
+ );
154
+
155
+ methods!(
156
+ EnveloperbEncryptedRecord,
157
+ rbself,
158
+
159
+ fn enveloperb_encrypted_record_new(serialized_record: RString) -> EnveloperbEncryptedRecord {
160
+ let s = serialized_record.unwrap().to_vec_u8_unchecked();
161
+ let ct = EncryptedRecord::from_vec(s).map_err(|e| VM::raise(Class::from_existing("ArgumentError"), &format!("Failed to decode encrypted record: {:?}", e))).unwrap();
162
+
163
+ let klass = Module::from_existing("Enveloperb").get_nested_class("EncryptedRecord");
164
+ return klass.wrap_data(ct, &*ENCRYPTED_RECORD_WRAPPER);
165
+ }
166
+
167
+ fn enveloperb_encrypted_record_serialize() -> RString {
168
+ let record = rbself.get_data(&*ENCRYPTED_RECORD_WRAPPER);
169
+
170
+ return RString::from_bytes(&record.to_vec().map_err(|e| VM::raise(Class::from_existing("RuntimeError"), &format!("Failed to encode encrypted record: {:?}", e))).unwrap(), &Encoding::find("BINARY").unwrap());
171
+ }
172
+ );
173
+
174
+ #[allow(non_snake_case)]
175
+ #[no_mangle]
176
+ pub extern "C" fn Init_enveloperb() {
177
+ Module::from_existing("Enveloperb").define(|envmod| {
178
+ envmod.define_nested_class("Simple", None).define(|klass| {
179
+ klass.singleton_class().def_private("_new", enveloperb_simple_new);
180
+ klass.def_private("_encrypt", enveloperb_simple_encrypt);
181
+ klass.def_private("_decrypt", enveloperb_simple_decrypt);
182
+ });
183
+
184
+ envmod.define_nested_class("AWSKMS", None).define(|klass| {
185
+ klass.singleton_class().def_private("_new", enveloperb_awskms_new);
186
+ klass.def_private("_encrypt", enveloperb_awskms_encrypt);
187
+ klass.def_private("_decrypt", enveloperb_awskms_decrypt);
188
+ });
189
+
190
+ envmod.define_nested_class("EncryptedRecord", None).define(|klass| {
191
+ klass.singleton_class().def_private("_new", enveloperb_encrypted_record_new);
192
+ klass.def_private("_serialize", enveloperb_encrypted_record_serialize);
193
+ });
194
+ });
195
+ }