enveloperb 0.1.1-x86_64-linux

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.
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
+ }
Binary file