aead 1.3.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.
@@ -0,0 +1,8 @@
1
+ /Gemfile.lock
2
+
3
+ /.yardoc
4
+ /bin
5
+ /coverage
6
+ /doc
7
+
8
+ *.bundle
@@ -0,0 +1,3 @@
1
+ rvm:
2
+ - 1.9.2
3
+ - 1.9.3
@@ -0,0 +1,2 @@
1
+ --markup-provider=redcarpet
2
+ --markup=markdown
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'terminal-notifier-guard'
6
+ gem 'pry'
7
+ gem 'pry-doc'
@@ -0,0 +1,10 @@
1
+ guard 'yard' do
2
+ watch(%r{lib/.+\.rb})
3
+ watch(%r{ext/.+\.c})
4
+ end
5
+
6
+ guard 'minitest' do
7
+ watch(%r|^spec/(.*)_spec\.rb|)
8
+ watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
9
+ watch(%r|^spec/spec_helper\.rb|) { "spec" }
10
+ end
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+ ===============
3
+
4
+ Copyright (c) 2012 OneLogin, Inc.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
@@ -0,0 +1,57 @@
1
+ # aead #
2
+
3
+ Ruby library for generating AEAD (authenticated encryption with
4
+ associated data) ciphertexts.
5
+
6
+ ## Description ##
7
+
8
+ Modern encryption best practices encourage the use of authenticated
9
+ encryption: ciphertext contents should be authenticated during the
10
+ decryption process, preventing either malicious or unintentional
11
+ silent corruption.
12
+
13
+ This library provides an extension to the Ruby OpenSSL bindings that
14
+ allows access to the GCM mode supported by OpenSSL (in versions higher
15
+ than 1.0.0). For Rubies linked against older versions of OpenSSL, a
16
+ mode is provided to perform AES-256-CTR with HMAC-SHA-256, as
17
+ [encouraged by Colin Percival](http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html).
18
+
19
+ ## Getting Started ##
20
+
21
+ ```ruby
22
+ require 'aead'
23
+
24
+ # currently, AES-256-GCM and AES-256-CTR-HMAC-SHA-256 are supported
25
+ mode = AEAD::Cipher.new('AES-256-GCM')
26
+ key = cipher.generate_key
27
+ nonce = cipher.generate_nonce
28
+
29
+ cipher = mode.new(key)
30
+ aead = cipher.encrypt(nonce, 'authentication data', 'plaintext')
31
+ plaintext = cipher.decrypt(nonce, 'authentication data', aead)
32
+ ```
33
+
34
+ If any of the key, nonce, authentication data, or ciphertext has been
35
+ altered, the ciphertext will fail to decrypt and an exception will be
36
+ raised.
37
+
38
+ ```ruby
39
+ cipher.decrypt(nonce, 'authentication data', aead.succ) # => ArgumentError
40
+ ```
41
+
42
+ ## Security Guidelines ##
43
+
44
+ Nonces should *never* be used to encrypt more than one plaintext with
45
+ the same key.
46
+
47
+ Nonces generated through the API provided by this gem are guaranteed
48
+ to be unique as long as the state file is not corrupted or
49
+ removed. The state file is located in `/var/tmp/ruby-aead`.
50
+
51
+ Nonce generation is thread-safe and tolerates being performed
52
+ simultaneously in separate processes.
53
+
54
+ If you believe you have discovered a security vulnerability in this
55
+ gem, please email `security@onelogin.com` with a description. We
56
+ follow responsible disclosure guidelines, and will work with you to
57
+ quickly find a resolution.
@@ -0,0 +1,46 @@
1
+ require 'rake/testtask'
2
+ require 'rake/version_task'
3
+ require 'yard'
4
+
5
+ task :default => %w{ build test }
6
+
7
+ task :build do
8
+ Dir.chdir('ext/openssl/cipher/aead') do
9
+ system %{ruby extconf.rb}
10
+ system %{make}
11
+ system %{cp aead.#{RbConfig::CONFIG['DLEXT']} ../../../../lib/openssl/cipher}
12
+ end
13
+ end
14
+
15
+ Rake::TestTask.new do |t|
16
+ t.libs.push 'lib'
17
+ t.libs.push 'spec'
18
+
19
+ t.test_files = FileList['spec/**/*_spec.rb']
20
+ t.verbose = true
21
+ end
22
+
23
+ if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'ruby'
24
+ require 'cane/rake_task'
25
+
26
+ task :default => :cane
27
+
28
+ Cane::RakeTask.new do |t|
29
+ t.add_threshold 'coverage/coverage.txt', :>=, 100
30
+ end
31
+ end
32
+
33
+ Rake::VersionTask.new do |t|
34
+ t.with_git_tag = true
35
+ end
36
+
37
+ YARD::Rake::YardocTask.new(:doc) do |t|
38
+ # --no-stats applies only to the `yard stats` command, so to include
39
+ # it we have to disable automatic stat generation and do it
40
+ # ourselves
41
+ t.options << '--no-stats'
42
+ t.after = lambda do
43
+ stats = YARD::CLI::Stats.new
44
+ stats.run '--list-undoc'
45
+ end
46
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.3.0
@@ -0,0 +1,34 @@
1
+ require 'pathname'
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'aead'
5
+ gem.version = Pathname.new(__FILE__).join('../VERSION').read.chomp
6
+
7
+ gem.author = 'Stephen Touset'
8
+ gem.email = 'stephen@touset.org'
9
+
10
+ gem.homepage = 'https://github.com/onelogin/aead'
11
+ gem.summary = %{Ruby library to generate AEADs}
12
+ gem.description = %{Ruby library to generate AEADs}
13
+
14
+ gem.bindir = 'script'
15
+ gem.files = `git ls-files` .split("\n")
16
+ gem.extensions = `git ls-files -- ext/*.rb`.split("\n")
17
+ gem.executables = `git ls-files -- script/*`.split("\n").map {|e| e[7..-1] }
18
+ gem.test_files = `git ls-files -- spec/*` .split("\n")
19
+
20
+ gem.add_dependency 'macaddr', '~> 1'
21
+
22
+ gem.add_development_dependency 'bundler'
23
+ gem.add_development_dependency 'cane'
24
+ gem.add_development_dependency 'guard'
25
+ gem.add_development_dependency 'guard-minitest'
26
+ gem.add_development_dependency 'guard-yard'
27
+ gem.add_development_dependency 'markdown'
28
+ gem.add_development_dependency 'minitest'
29
+ gem.add_development_dependency 'rake'
30
+ gem.add_development_dependency 'redcarpet'
31
+ gem.add_development_dependency 'simplecov'
32
+ gem.add_development_dependency 'yard'
33
+ gem.add_development_dependency 'version'
34
+ end
@@ -0,0 +1,4 @@
1
+ /Makefile
2
+ /*.bundle
3
+ /*.log
4
+ /*.o
@@ -0,0 +1,156 @@
1
+ #include <ruby.h>
2
+ #include <openssl/ssl.h>
3
+ #include <openssl/err.h>
4
+
5
+ VALUE dOSSL;
6
+ VALUE eCipherError;
7
+
8
+ #define GetCipherInit(obj, ctx) do { \
9
+ Data_Get_Struct((obj), EVP_CIPHER_CTX, (ctx)); \
10
+ } while (0)
11
+
12
+ #define GetCipher(obj, ctx) do { \
13
+ GetCipherInit((obj), (ctx)); \
14
+ if (!(ctx)) { \
15
+ ossl_raise(rb_eRuntimeError, "Cipher not inititalized!"); \
16
+ } \
17
+ } while (0)
18
+
19
+ static VALUE
20
+ ossl_make_error(VALUE exc, const char *fmt, va_list args)
21
+ {
22
+ char buf[BUFSIZ];
23
+ const char *msg;
24
+ long e;
25
+ int len = 0;
26
+
27
+ #ifdef HAVE_ERR_PEEK_LAST_ERROR
28
+ e = ERR_peek_last_error();
29
+ #else
30
+ e = ERR_peek_error();
31
+ #endif
32
+ if (fmt) {
33
+ len = vsnprintf(buf, BUFSIZ, fmt, args);
34
+ }
35
+ if (len < BUFSIZ && e) {
36
+ if (dOSSL == Qtrue) /* FULL INFO */
37
+ msg = ERR_error_string(e, NULL);
38
+ else
39
+ msg = ERR_reason_error_string(e);
40
+ len += snprintf(buf+len, BUFSIZ-len, "%s%s", (len ? ": " : ""), msg);
41
+ }
42
+ if (dOSSL == Qtrue){ /* show all errors on the stack */
43
+ while ((e = ERR_get_error()) != 0){
44
+ rb_warn("error on stack: %s", ERR_error_string(e, NULL));
45
+ }
46
+ }
47
+ ERR_clear_error();
48
+
49
+ if(len > BUFSIZ) len = rb_long2int(strlen(buf));
50
+ return rb_exc_new(exc, buf, len);
51
+ }
52
+
53
+ void
54
+ ossl_raise(VALUE exc, const char *fmt, ...)
55
+ {
56
+ va_list args;
57
+ VALUE err;
58
+ va_start(args, fmt);
59
+ err = ossl_make_error(exc, fmt, args);
60
+ va_end(args);
61
+ rb_exc_raise(err);
62
+ }
63
+
64
+ static VALUE
65
+ ossl_cipher_set_aad(VALUE self, VALUE data)
66
+ {
67
+ EVP_CIPHER_CTX *ctx;
68
+ char *in = NULL;
69
+ int in_len = 0;
70
+ int out_len = 0;
71
+
72
+ StringValue(data);
73
+
74
+ in = (unsigned char *) RSTRING_PTR(data);
75
+ in_len = RSTRING_LENINT(data);
76
+
77
+ GetCipher(self, ctx);
78
+
79
+ if (!EVP_CipherUpdate(ctx, NULL, &out_len, in, in_len))
80
+ ossl_raise(eCipherError, NULL);
81
+
82
+ return self;
83
+ }
84
+
85
+ static VALUE
86
+ ossl_cipher_get_tag(VALUE self)
87
+ {
88
+ EVP_CIPHER_CTX *ctx;
89
+ VALUE tag;
90
+
91
+ tag = rb_str_new(NULL, 16);
92
+
93
+ GetCipher(self, ctx);
94
+
95
+ #ifndef EVP_CTRL_GCM_GET_TAG
96
+ ossl_raise(eCipherError, "your version of OpenSSL doesn't support GCM");
97
+ #else
98
+ if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, (unsigned char *)RSTRING_PTR(tag)))
99
+ ossl_raise(eCipherError, NULL);
100
+ #endif
101
+
102
+ return tag;
103
+ }
104
+
105
+ static VALUE
106
+ ossl_cipher_set_tag(VALUE self, VALUE data)
107
+ {
108
+ EVP_CIPHER_CTX *ctx;
109
+ char *in = NULL;
110
+ int in_len = 0;
111
+
112
+ StringValue(data);
113
+
114
+ in = (unsigned char *) RSTRING_PTR(data);
115
+ in_len = RSTRING_LENINT(data);
116
+
117
+ GetCipher(self, ctx);
118
+
119
+ #ifndef EVP_CTRL_GCM_SET_TAG
120
+ ossl_raise(eCipherError, "your version of OpenSSL doesn't support GCM");
121
+ #else
122
+ if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, in_len, in))
123
+ ossl_raise(eCipherError, NULL);
124
+ #endif
125
+
126
+ return data;
127
+ }
128
+
129
+ static VALUE
130
+ ossl_cipher_verify(VALUE self)
131
+ {
132
+ EVP_CIPHER_CTX *ctx;
133
+ int out_len = 0;
134
+
135
+ GetCipher(self, ctx);
136
+
137
+ if (!EVP_CipherUpdate(ctx, NULL, &out_len, NULL, 0))
138
+ ossl_raise(eCipherError, "ciphertext failed authentication step");
139
+
140
+ return rb_str_new(0, 0);
141
+ }
142
+
143
+ void
144
+ Init_aead(void)
145
+ {
146
+ VALUE mOSSL = rb_define_module("OpenSSL");
147
+ VALUE mOSSLCipher = rb_define_class_under(mOSSL, "Cipher", rb_cObject);
148
+ VALUE eOSSLError = rb_define_class_under(mOSSL,"OpenSSLError",rb_eStandardError);
149
+
150
+ eCipherError = rb_define_class_under(mOSSLCipher, "CipherError", eOSSLError);
151
+
152
+ rb_define_method(mOSSLCipher, "aad=", ossl_cipher_set_aad, 1);
153
+ rb_define_method(mOSSLCipher, "gcm_tag", ossl_cipher_get_tag, 0);
154
+ rb_define_method(mOSSLCipher, "gcm_tag=", ossl_cipher_set_tag, 1);
155
+ rb_define_method(mOSSLCipher, "verify", ossl_cipher_verify, 0);
156
+ }
@@ -0,0 +1,6 @@
1
+ require 'mkmf'
2
+
3
+ have_header("openssl/ssl.h")
4
+ have_library("ssl", "SSLv23_method")
5
+
6
+ create_makefile('openssl/cipher/aead')
@@ -0,0 +1,4 @@
1
+ module AEAD
2
+ autoload :Cipher, 'aead/cipher'
3
+ autoload :Nonce, 'aead/nonce'
4
+ end
@@ -0,0 +1,221 @@
1
+ require 'aead'
2
+
3
+ require 'openssl'
4
+ require 'openssl/cipher/aead'
5
+ require 'securerandom'
6
+
7
+ #
8
+ # Wraps AEAD ciphers in a simplified interface.
9
+ #
10
+ class AEAD::Cipher
11
+ autoload :AES_256_GCM, 'aead/cipher/aes_256_gcm'
12
+ autoload :AES_256_CBC_HMAC_SHA_256, 'aead/cipher/aes_256_cbc_hmac_sha_256'
13
+ autoload :AES_256_CTR_HMAC_SHA_256, 'aead/cipher/aes_256_ctr_hmac_sha_256'
14
+
15
+ #
16
+ # Returns a particular Cipher implementation.
17
+ #
18
+ # @param [String] algorithm the AEAD implementation to use
19
+ # @return [Class] the cipher implementation
20
+ #
21
+ def self.new(algorithm)
22
+ # run normal Class#new if we're being called from a subclass
23
+ return super unless self == AEAD::Cipher
24
+
25
+ # TODO: ciphers should register themselves, as opposed to using a
26
+ # potentiall-unsafe const_get
27
+ self.const_get algorithm.tr('-', '_').upcase
28
+ end
29
+
30
+ #
31
+ # Returns a securely-generated key of appropriate length for the
32
+ # current Cipher.
33
+ #
34
+ # @return [String] a random key
35
+ #
36
+ def self.generate_key
37
+ SecureRandom.random_bytes(self.key_len)
38
+ end
39
+
40
+ #
41
+ # Returns a unique nonce for the current Cipher.
42
+ #
43
+ # @return [String] a random key
44
+ #
45
+ def self.generate_nonce
46
+ AEAD::Nonce.generate
47
+ end
48
+
49
+ #
50
+ # Does a constant-time comparison between two strings. Useful to
51
+ # avoid timing attacks when comparing a generated signature against
52
+ # a user-provided signature.
53
+ #
54
+ # @param [String] left any string
55
+ # @param [String] right any string
56
+ # @return [Boolean] whether or not the strings are equal
57
+ #
58
+ def self.signature_compare(left, right)
59
+ # short-circuit if the lengths are inequal
60
+ return false if left.to_s.bytesize != right.bytesize
61
+
62
+ # Constant-time string comparison algorithm:
63
+ # 1. Break both strings into bytes
64
+ # 2. Subtract the strings from one-another, byte by byte
65
+ # (any non-equal bytes will subtract to a nonzero value)
66
+ # 3. OR the XOR'd bytes together
67
+ # 4. If the result is nonzero, the strings differed.
68
+ left = left.bytes.to_a
69
+ right = right.bytes.to_a
70
+ result = 0
71
+
72
+ left.length.times do |i|
73
+ result |= left[i] - right[i]
74
+ end
75
+
76
+ result == 0
77
+ end
78
+
79
+ #
80
+ # The length of keys of encryption keys used by the current Cipher.
81
+ #
82
+ # @return [Integer] the length of keys in bytes
83
+ #
84
+ def key_len
85
+ self.class.key_len
86
+ end
87
+
88
+ #
89
+ # The length of nonces used by the current Cipher.
90
+ #
91
+ # @return [Integer] the length of nonces in bytes
92
+ #
93
+ def nonce_len
94
+ self.class.nonce_len
95
+ end
96
+
97
+ #
98
+ # Encrypts a plaintext using the current Cipher.
99
+ #
100
+ # IMPORTANT: Do not ever encrypt data using the same nonce more than
101
+ # once given a particular secret key. Doing so will violate the
102
+ # security guarantees of the AEAD cipher.
103
+ #
104
+ # @param [String] nonce a unique nonce, never before used with the
105
+ # current encryption key
106
+ # @param [String, nil] aad arbitrary additional authentication data that
107
+ # must later be provided to decrypt the aead
108
+ # @param [String] plaintext arbitrary plaintext
109
+ # @return [String] the generated AEAD
110
+ #
111
+ def encrypt(nonce, aad, plaintext)
112
+ _verify_nonce_bytesize(nonce, self.nonce_len)
113
+ _verify_plaintext_presence(plaintext)
114
+
115
+ self._encrypt(
116
+ _pad_nonce(nonce),
117
+ aad,
118
+ plaintext
119
+ )
120
+ end
121
+
122
+ #
123
+ # Decrypts a plaintext using the current Cipher.
124
+ #
125
+ # @param [String] nonce the nonce used when encrypting the AEAD
126
+ # @param [String] aad the additional authentication data used when
127
+ # encrypting the AEAD
128
+ # @param [String] aead the encrypted AEAD
129
+ # @return [String] the original plaintext
130
+ #
131
+ def decrypt(nonce, aad, aead)
132
+ _verify_nonce_bytesize(nonce, self.nonce_len)
133
+
134
+ self._decrypt(
135
+ _pad_nonce(nonce),
136
+ aad,
137
+ _extract_ciphertext(aead, self.tag_len),
138
+ _extract_tag(aead, self.tag_len)
139
+ )
140
+ end
141
+
142
+ protected
143
+
144
+ # The OpenSSL algorithm to be used by the cipher.
145
+ attr_accessor :algorithm
146
+
147
+ # The secret key provided by the user.
148
+ attr_accessor :key
149
+
150
+ #
151
+ # Initializes the cipher.
152
+ #
153
+ # @param [String] algorithm the full encryption mode to be used in
154
+ # calls to {#cipher}.
155
+ # @param [String] key the encryption key supplied by the user
156
+ # @return [Cipher] the initialized Cipher
157
+ #
158
+ def initialize(algorithm, key)
159
+ _verify_key_bytesize(key, self.key_len)
160
+
161
+ self.algorithm = algorithm.dup.freeze
162
+ self.key = key.dup.freeze
163
+
164
+ self.freeze
165
+ end
166
+
167
+
168
+ #
169
+ # Yields the {OpenSSL::Cipher} for the current {#algorithm}
170
+ #
171
+ def cipher(direction)
172
+ yield OpenSSL::Cipher.new(algorithm).send(direction)
173
+ end
174
+
175
+ #
176
+ # The length of initialization vectors used by the current Cipher.
177
+ #
178
+ # @return [Integer] the length of initialization vectors in bytes
179
+ #
180
+ def iv_len
181
+ self.class.iv_len
182
+ end
183
+
184
+ #
185
+ # The length of authentication tags generated by the current Cipher.
186
+ #
187
+ # @return [Integer] the length of authentication tags in bytes
188
+ #
189
+ def tag_len
190
+ self.class.tag_len
191
+ end
192
+
193
+ private
194
+
195
+ def _verify_key_bytesize(key, key_len)
196
+ raise ArgumentError, "key must be at least #{key_len} bytes" unless
197
+ key.bytesize >= key_len
198
+ end
199
+
200
+ def _verify_nonce_bytesize(nonce, nonce_len)
201
+ raise ArgumentError, "nonce must be at least #{nonce_len} bytes" unless
202
+ nonce.bytesize == nonce_len
203
+ end
204
+
205
+ def _verify_plaintext_presence(plaintext)
206
+ raise ArgumentError, 'plaintext must not be empty' unless
207
+ not plaintext.nil? and not plaintext.empty?
208
+ end
209
+
210
+ def _pad_nonce(nonce)
211
+ nonce.rjust(self.iv_len, "\0")
212
+ end
213
+
214
+ def _extract_ciphertext(ciphertext, tag_len)
215
+ ciphertext[ 0 .. -tag_len - 1 ].to_s
216
+ end
217
+
218
+ def _extract_tag(ciphertext, tag_len)
219
+ ciphertext[ -tag_len .. -1 ].to_s
220
+ end
221
+ end