miscreant 0.0.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c3b2740135b6e13589326c9b8f2c926edff86516
4
- data.tar.gz: b4327cc7c72ce38aaec0208f967c67fdceb2508f
3
+ metadata.gz: 23230bd423fada7ae6bfceeafeb1bff7133e8e75
4
+ data.tar.gz: 3dc5b9db54803f64f9e58a87a4ac89370e785b2e
5
5
  SHA512:
6
- metadata.gz: b24730c57fd7ef67a4e7699a63c05454875ffec00366212a31445111525b7fbd953d122f8ff88e1871e0f7caee792780394382b7b9021fb9a83772fbf8d2ad6f
7
- data.tar.gz: ceafedfa57c9b6d6ca7a72d8d50e31041af4d9c5113ea7e2d6482ef49a456914411138d6a75f43f5f67340f8aed5fe71bac0a5bbe7581734c2379681e3e1cf15
6
+ metadata.gz: c53969bd291002daf380591ee6547c49fa24e86e7b3e9c3fdb5c4575cbfa049a1db141af95f447dc8e4bf5a35a9acd053138a4cea1ee940c635cb44e2b0d50a9
7
+ data.tar.gz: 45871b8499fbf30d1090a4f45a31eaf6d54583f7e458013c701597a7ec28c2d7df8aac80b94adf717baf1b2b64a9233dc77fd7294595e744b948ae08df7c92c7
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # miscreant.rb [![Latest Version][gem-shield]][gem-link] [![Build Status][build-image]][build-link] [![Code Climate][codeclimate-image]][codeclimate-link] [![MIT licensed][license-image]][license-link]
1
+ # miscreant.rb [![Latest Version][gem-shield]][gem-link] [![Build Status][build-image]][build-link] [![Code Climate][codeclimate-image]][codeclimate-link] [![MIT licensed][license-image]][license-link] [![Gitter Chat][gitter-image]][gitter-link]
2
2
 
3
3
  [gem-shield]: https://badge.fury.io/rb/miscreant.svg
4
4
  [gem-link]: https://rubygems.org/gems/miscreant
@@ -8,11 +8,13 @@
8
8
  [codeclimate-link]: https://codeclimate.com/github/miscreant/miscreant
9
9
  [license-image]: https://img.shields.io/badge/license-MIT-blue.svg
10
10
  [license-link]: https://github.com/miscreant/miscreant/blob/master/LICENSE.txt
11
+ [gitter-image]: https://badges.gitter.im/badge.svg
12
+ [gitter-link]: https://gitter.im/miscreant/Lobby
11
13
 
12
14
  > The best crypto you've never heard of, brought to you by [Phil Rogaway]
13
15
 
14
16
  Ruby implementation of **Miscreant**: Advanced symmetric encryption using the
15
- AES-SIV ([RFC 5297]) and [CHAIN] constructions, providing easy-to-use (or
17
+ AES-SIV ([RFC 5297]) and [CHAIN/STREAM] constructions, providing easy-to-use (or
16
18
  rather, hard-to-misuse) encryption of individual messages or message streams.
17
19
 
18
20
  **AES-SIV** provides [nonce-reuse misuse-resistance] (NRMR): accidentally
@@ -28,7 +30,7 @@ For more information, see the [toplevel README.md].
28
30
  [Phil Rogaway]: https://en.wikipedia.org/wiki/Phillip_Rogaway
29
31
  [AES-SIV]: https://www.iacr.org/archive/eurocrypt2006/40040377/40040377.pdf
30
32
  [RFC 5297]: https://tools.ietf.org/html/rfc5297
31
- [CHAIN]: http://web.cs.ucdavis.edu/~rogaway/papers/oae.pdf
33
+ [CHAIN/STREAM]: http://web.cs.ucdavis.edu/~rogaway/papers/oae.pdf
32
34
  [nonce-reuse misuse-resistance]: https://www.lvh.io/posts/nonce-misuse-resistance-101.html
33
35
  [AES-GCM]: https://en.wikipedia.org/wiki/Galois/Counter_Mode
34
36
  [chosen ciphertext attacks]: https://en.wikipedia.org/wiki/Chosen-ciphertext_attack
@@ -39,11 +41,11 @@ For more information, see the [toplevel README.md].
39
41
  Have questions? Want to suggest a feature or change?
40
42
 
41
43
  * [Gitter]: web-based chat about miscreant projects including **miscreant.rb**
42
- * [Google Group]: join via web or email ([miscreant+subscribe@googlegroups.com])
44
+ * [Google Group]: join via web or email ([miscreant-crypto+subscribe@googlegroups.com])
43
45
 
44
46
  [Gitter]: https://gitter.im/miscreant/Lobby
45
- [Google Group]: https://groups.google.com/forum/#!forum/miscreant
46
- [miscreant+subscribe@googlegroups.com]: mailto:miscreant+subscribe@googlegroups.com
47
+ [Google Group]: https://groups.google.com/forum/#!forum/miscreant-crypto
48
+ [miscreant-crypto+subscribe@googlegroups.com]: mailto:miscreant-crypto+subscribe@googlegroups.com?subject=subscribe
47
49
 
48
50
  ## Security Notice
49
51
 
@@ -135,8 +137,8 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/miscre
135
137
 
136
138
  ## Copyright
137
139
 
138
- Copyright (c) 2013-2017 John Downey, [The Miscreant Developers][AUTHORS].
140
+ Copyright (c) 2017 [The Miscreant Developers][AUTHORS].
139
141
  See [LICENSE.txt] for further details.
140
142
 
141
143
  [AUTHORS]: https://github.com/miscreant/miscreant/blob/master/AUTHORS.md
142
- [LICENSE.txt]: https://github.com/miscreant/miscreant/blob/master/ruby/LICENSE.txt
144
+ [LICENSE.txt]: https://github.com/miscreant/miscreant/blob/master/LICENSE.txt
@@ -4,13 +4,9 @@ require "openssl"
4
4
  require "securerandom"
5
5
 
6
6
  require "miscreant/version"
7
+ require "miscreant/internals"
7
8
 
8
- require "miscreant/aes"
9
- require "miscreant/aes/siv"
10
- require "miscreant/aes/cmac"
11
- require "miscreant/util"
12
-
13
- # Misuse-resistant symmetric encryption using the AES-SIV (RFC 5297) and CHAIN constructions
9
+ # Miscreant: A misuse-resistant symmetric encryption library
14
10
  module Miscreant
15
11
  # Parent of all cryptography-related errors
16
12
  CryptoError = Class.new(StandardError)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "miscreant/internals/aes"
4
+ require "miscreant/internals/aes/siv"
5
+ require "miscreant/internals/aes/cmac"
6
+ require "miscreant/internals/util"
7
+
8
+ module Miscreant
9
+ # Internal functionality not intended for direct consumption
10
+ module Internals # :nodoc:
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ module Internals
6
+ # The Advanced Encryption Standard Block Cipher
7
+ module AES # :nodoc:
8
+ # Size of an AES block (i.e. input/output from the AES function)
9
+ BLOCK_SIZE = 16
10
+
11
+ # A bytestring of all zeroes, the same length as an AES block
12
+ ZERO_BLOCK = ("\0" * BLOCK_SIZE).freeze
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,79 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ module Internals
6
+ module AES
7
+ # The AES-CMAC message authentication code
8
+ class CMAC # :nodoc:
9
+ # Create a new AES-CMAC instance
10
+ #
11
+ # @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
12
+ #
13
+ # @return [Miscreant::AES::CMAC] new AES-CMAC instance
14
+ def initialize(key)
15
+ raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
16
+ raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
17
+ raise ArgumentError, "key must be 32 or 64 bytes" unless [16, 32].include?(key.length)
18
+
19
+ # The only valid use of ECB mode: constructing higher-level cryptographic primitives
20
+ @cipher = OpenSSL::Cipher.new("AES-#{key.length * 8}-ECB")
21
+ @cipher.encrypt
22
+ @cipher.padding = 0
23
+ @cipher.key = key
24
+ @key1, @key2 = _generate_subkeys
25
+ end
26
+
27
+ # Inspect this AES-CMAC instance
28
+ #
29
+ # @return [String] description of this instance
30
+ def inspect
31
+ to_s
32
+ end
33
+
34
+ # Compute the AES-CMAC of the given input message in a single shot,
35
+ # outputting the MAC tag.
36
+ #
37
+ # Unlike other AES-CMAC implementations, this one does not support
38
+ # incremental processing/IUF operation. (Though that would enable
39
+ # slightly more efficient decryption for AES-SIV)
40
+ #
41
+ # @param message [String] an Encoding::BINARY string to authenticate
42
+ #
43
+ # @return [String] CMAC tag
44
+ def digest(message)
45
+ raise TypeError, "expected String, got #{message.class}" unless message.is_a?(String)
46
+ raise ArgumentError, "message must be Encoding::BINARY" unless message.encoding == Encoding::BINARY
47
+
48
+ if message.empty? || message.length % AES::BLOCK_SIZE != 0
49
+ message = Util.pad(message, AES::BLOCK_SIZE)
50
+ final_block = @key2
51
+ else
52
+ final_block = @key1
53
+ end
54
+
55
+ count = message.length / AES::BLOCK_SIZE
56
+ result = AES::ZERO_BLOCK
57
+
58
+ count.times do |i|
59
+ block = message.slice(AES::BLOCK_SIZE * i, AES::BLOCK_SIZE)
60
+ block = Util.xor(final_block, block) if i == count - 1
61
+ block = Util.xor(block, result)
62
+ result = @cipher.update(block) + @cipher.final
63
+ end
64
+
65
+ result
66
+ end
67
+
68
+ private
69
+
70
+ def _generate_subkeys
71
+ key0 = @cipher.update(AES::ZERO_BLOCK) + @cipher.final
72
+ key1 = Util.dbl(key0)
73
+ key2 = Util.dbl(key1)
74
+ [key1, key2]
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,120 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ module Internals
6
+ module AES
7
+ # The AES-SIV misuse resistant authenticated encryption cipher
8
+ class SIV # :nodoc:
9
+ # Generate a new random AES-SIV key of the given size
10
+ #
11
+ # @param size [Integer] size of key in bytes (32 or 64)
12
+ #
13
+ # @return [String] newly generated AES-SIV key
14
+ def self.generate_key(size = 32)
15
+ raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
16
+ SecureRandom.random_bytes(size)
17
+ end
18
+
19
+ # Create a new AES-SIV instance
20
+ #
21
+ # @param key [String] 32-byte or 64-byte Encoding::BINARY cryptographic key
22
+ #
23
+ # @return [Miscreant::AES::SIV] new AES-SIV instance
24
+ def initialize(key)
25
+ raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
26
+ raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
27
+ raise ArgumentError, "key must be 32 or 64 bytes" unless [32, 64].include?(key.length)
28
+
29
+ length = key.length / 2
30
+
31
+ @mac_key = key.slice(0, length)
32
+ @enc_key = key.slice(length..-1)
33
+ end
34
+
35
+ # Inspect this AES-SIV instance
36
+ #
37
+ # @return [String] description of this instance
38
+ def inspect
39
+ to_s
40
+ end
41
+
42
+ # Encrypt a message using AES-SIV, authenticating it along with the associated data
43
+ #
44
+ # @param plaintext [String] an Encoding::BINARY string to encrypt
45
+ # @param associated_data [Array<String>] optional array of message headers to authenticate
46
+ #
47
+ # @return [String] encrypted ciphertext
48
+ def seal(plaintext, associated_data = [])
49
+ raise TypeError, "expected String, got #{plaintext.class}" unless plaintext.is_a?(String)
50
+ raise ArgumentError, "plaintext must be Encoding::BINARY" unless plaintext.encoding == Encoding::BINARY
51
+
52
+ v = _s2v(associated_data, plaintext)
53
+ ciphertext = _transform(v, plaintext)
54
+ v + ciphertext
55
+ end
56
+
57
+ # Verify and decrypt an AES-SIV ciphertext, authenticating it along with the associated data
58
+ #
59
+ # @param ciphertext [String] an Encoding::BINARY string to decrypt
60
+ # @param associated_data [Array<String>] optional array of message headers to authenticate
61
+ #
62
+ # @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
63
+ # @return [String] decrypted plaintext
64
+ def open(ciphertext, associated_data = [])
65
+ raise TypeError, "expected String, got #{ciphertext.class}" unless ciphertext.is_a?(String)
66
+ raise ArgumentError, "ciphertext must be Encoding::BINARY" unless ciphertext.encoding == Encoding::BINARY
67
+
68
+ v = ciphertext.slice(0, AES::BLOCK_SIZE)
69
+ ciphertext = ciphertext.slice(AES::BLOCK_SIZE..-1)
70
+ plaintext = _transform(v, ciphertext)
71
+
72
+ t = _s2v(associated_data, plaintext)
73
+ raise IntegrityError, "ciphertext verification failure!" unless Util.ct_equal(t, v)
74
+
75
+ plaintext
76
+ end
77
+
78
+ private
79
+
80
+ # Performs raw unauthenticted encryption or decryption of the message
81
+ def _transform(v, data)
82
+ return "".b if data.empty?
83
+
84
+ cipher = OpenSSL::Cipher::AES.new(@mac_key.length * 8, :CTR)
85
+ cipher.encrypt
86
+ cipher.iv = Util.zero_iv_bits(v)
87
+ cipher.key = @enc_key
88
+ cipher.update(data) + cipher.final
89
+ end
90
+
91
+ # The S2V operation consists of the doubling and XORing of the outputs
92
+ # of the pseudo-random function CMAC.
93
+ #
94
+ # See Section 2.4 of RFC 5297 for more information
95
+ def _s2v(associated_data, plaintext)
96
+ # Note: the standalone S2V returns CMAC(1) if the number of passed
97
+ # vectors is zero, however in SIV construction this case is never
98
+ # triggered, since we always pass plaintext as the last vector (even
99
+ # if it's zero-length), so we omit this case.
100
+ cmac = CMAC.new(@mac_key)
101
+ d = cmac.digest(AES::ZERO_BLOCK)
102
+
103
+ associated_data.each do |ad|
104
+ d = Util.dbl(d)
105
+ d = Util.xor(d, cmac.digest(ad))
106
+ end
107
+
108
+ if plaintext.bytesize >= AES::BLOCK_SIZE
109
+ d = Util.xorend(plaintext, d)
110
+ else
111
+ d = Util.dbl(d)
112
+ d = Util.xor(d, Util.pad(plaintext, AES::BLOCK_SIZE))
113
+ end
114
+
115
+ cmac.digest(d)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,83 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ module Internals
6
+ # Internal utility functions
7
+ module Util # :nodoc:
8
+ module_function
9
+
10
+ # Perform a doubling operation as described in the CMAC and SIV papers
11
+ def dbl(value)
12
+ overflow = 0
13
+ words = value.unpack("N4").reverse
14
+
15
+ words.map! do |word|
16
+ new_word = (word << 1) & 0xFFFFFFFF
17
+ new_word |= overflow
18
+ overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
19
+ new_word
20
+ end
21
+
22
+ result = words.reverse.pack("N4")
23
+ result[-1] = (result[-1].ord ^ select(overflow, 0x87, 0)).chr
24
+ result
25
+ end
26
+
27
+ # Pad value with a 0x80 value and zeroes up to the given length
28
+ def pad(message, length)
29
+ padded_length = message.length + length - (message.length % length)
30
+ message += "\x80"
31
+ message.ljust(padded_length, "\0")
32
+ end
33
+
34
+ # Perform a constant time(-ish) branch operation
35
+ def select(subject, result_if_one, result_if_zero)
36
+ (~(subject - 1) & result_if_one) | ((subject - 1) & result_if_zero)
37
+ end
38
+
39
+ # Perform an xor on arbitrary bytestrings
40
+ def xor(a, b)
41
+ length = [a.length, b.length].min
42
+ output = "\0" * length
43
+ length.times do |i|
44
+ output[i] = (a[i].ord ^ b[i].ord).chr
45
+ end
46
+ output
47
+ end
48
+
49
+ # XOR the second value into the end of the first
50
+ def xorend(a, b)
51
+ difference = a.length - b.length
52
+
53
+ left = a.slice(0, difference)
54
+ right = a.slice(difference..-1)
55
+
56
+ left + xor(right, b)
57
+ end
58
+
59
+ # Zero out the top bits in the last 32-bit words of the IV
60
+ def zero_iv_bits(iv)
61
+ # "We zero-out the top bit in each of the last two 32-bit words
62
+ # of the IV before assigning it to Ctr"
63
+ # -- http://web.cs.ucdavis.edu/~rogaway/papers/siv.pdf
64
+ iv = iv.dup
65
+ iv[8] = (iv[8].ord & 0x7f).chr
66
+ iv[12] = (iv[12].ord & 0x7f).chr
67
+ iv
68
+ end
69
+
70
+ # Perform a constant time-ish comparison of two bytestrings
71
+ def ct_equal(a, b)
72
+ return false unless a.bytesize == b.bytesize
73
+
74
+ l = a.unpack("C*")
75
+ r = 0
76
+ i = -1
77
+
78
+ b.each_byte { |v| r |= v ^ l[i += 1] }
79
+ r.zero?
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Miscreant
4
- VERSION = "0.0.0"
4
+ VERSION = "0.1.0"
5
5
  end
@@ -7,13 +7,18 @@ require "miscreant/version"
7
7
 
8
8
  Gem::Specification.new do |spec|
9
9
  spec.name = "miscreant"
10
- spec.version = Miscreant::VERSION
11
10
  spec.authors = ["Tony Arcieri"]
12
11
  spec.email = ["bascule@gmail.com"]
12
+ spec.summary = "Misuse-resistant authenticated symmetric encryption"
13
+ spec.description = <<-DESCRIPTION.strip.gsub(/\s+/, " ")
14
+ Misuse-resistant symmetric encryption using the AES-SIV (RFC 5297)
15
+ and CHAIN/STREAM constructions.
16
+ DESCRIPTION
17
+
18
+ spec.version = Miscreant::VERSION
13
19
  spec.licenses = ["MIT"]
14
20
  spec.homepage = "https://github.com/miscreant/miscreant/tree/master/ruby/"
15
- spec.summary = "Misuse-resistant authenticated symmetric encryption"
16
- spec.description = "Misuse-resistant symmetric encryption using the AES-SIV (RFC 5297) and CHAIN constructions"
21
+ spec.description = ""
17
22
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
23
  spec.bindir = "exe"
19
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: miscreant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-07-24 00:00:00.000000000 Z
11
+ date: 2017-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -24,8 +24,7 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.14'
27
- description: Misuse-resistant symmetric encryption using the AES-SIV (RFC 5297) and
28
- CHAIN constructions
27
+ description: ''
29
28
  email:
30
29
  - bascule@gmail.com
31
30
  executables: []
@@ -37,14 +36,14 @@ files:
37
36
  - ".rubocop.yml"
38
37
  - ".ruby-version"
39
38
  - Gemfile
40
- - LICENSE.txt
41
39
  - README.md
42
40
  - Rakefile
43
41
  - lib/miscreant.rb
44
- - lib/miscreant/aes.rb
45
- - lib/miscreant/aes/cmac.rb
46
- - lib/miscreant/aes/siv.rb
47
- - lib/miscreant/util.rb
42
+ - lib/miscreant/internals.rb
43
+ - lib/miscreant/internals/aes.rb
44
+ - lib/miscreant/internals/aes/cmac.rb
45
+ - lib/miscreant/internals/aes/siv.rb
46
+ - lib/miscreant/internals/util.rb
48
47
  - lib/miscreant/version.rb
49
48
  - miscreant.gemspec
50
49
  homepage: https://github.com/miscreant/miscreant/tree/master/ruby/
@@ -1,19 +0,0 @@
1
- Copyright (c) 2013-2017 John Downey, The Miscreant Developers
2
-
3
- Permission is hereby granted, free of charge, to any person obtaining a copy
4
- of this software and associated documentation files (the "Software"), to deal
5
- in the Software without restriction, including without limitation the rights
6
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- copies of the Software, and to permit persons to whom the Software is
8
- furnished to do so, subject to the following conditions:
9
-
10
- The above copyright notice and this permission notice shall be included in
11
- all copies or substantial portions of the Software.
12
-
13
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- THE SOFTWARE.
@@ -1,13 +0,0 @@
1
- # encoding: binary
2
- # frozen_string_literal: true
3
-
4
- module Miscreant
5
- # The Advanced Encryption Standard Block Cipher
6
- module AES
7
- # Size of an AES block (i.e. input/output from the AES function)
8
- BLOCK_SIZE = 16
9
-
10
- # A bytestring of all zeroes, the same length as an AES block
11
- ZERO_BLOCK = ("\0" * BLOCK_SIZE).freeze
12
- end
13
- end
@@ -1,77 +0,0 @@
1
- # encoding: binary
2
- # frozen_string_literal: true
3
-
4
- module Miscreant
5
- module AES
6
- # The AES-CMAC message authentication code
7
- class CMAC
8
- # Create a new AES-CMAC instance
9
- #
10
- # @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
11
- #
12
- # @return [Miscreant::AES::CMAC] new AES-CMAC instance
13
- def initialize(key)
14
- raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
15
- raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
16
- raise ArgumentError, "key must be 32 or 64 bytes" unless [16, 32].include?(key.length)
17
-
18
- # The only valid use of ECB mode: constructing higher-level cryptographic primitives
19
- @cipher = OpenSSL::Cipher.new("AES-#{key.length * 8}-ECB")
20
- @cipher.encrypt
21
- @cipher.padding = 0
22
- @cipher.key = key
23
- @key1, @key2 = _generate_subkeys
24
- end
25
-
26
- # Inspect this AES-CMAC instance
27
- #
28
- # @return [String] description of this instance
29
- def inspect
30
- to_s
31
- end
32
-
33
- # Compute the AES-CMAC of the given input message in a single shot,
34
- # outputting the MAC tag.
35
- #
36
- # Unlike other AES-CMAC implementations, this one does not support
37
- # incremental processing/IUF operation. (Though that would enable
38
- # slightly more efficient decryption for AES-SIV)
39
- #
40
- # @param message [String] an Encoding::BINARY string to authenticate
41
- #
42
- # @return [String] CMAC tag
43
- def digest(message)
44
- raise TypeError, "expected String, got #{message.class}" unless message.is_a?(String)
45
- raise ArgumentError, "message must be Encoding::BINARY" unless message.encoding == Encoding::BINARY
46
-
47
- if message.empty? || message.length % AES::BLOCK_SIZE != 0
48
- message = Util.pad(message, AES::BLOCK_SIZE)
49
- final_block = @key2
50
- else
51
- final_block = @key1
52
- end
53
-
54
- count = message.length / AES::BLOCK_SIZE
55
- result = AES::ZERO_BLOCK
56
-
57
- count.times do |i|
58
- block = message.slice(AES::BLOCK_SIZE * i, AES::BLOCK_SIZE)
59
- block = Util.xor(final_block, block) if i == count - 1
60
- block = Util.xor(block, result)
61
- result = @cipher.update(block) + @cipher.final
62
- end
63
-
64
- result
65
- end
66
-
67
- private
68
-
69
- def _generate_subkeys
70
- key0 = @cipher.update(AES::ZERO_BLOCK) + @cipher.final
71
- key1 = Util.dbl(key0)
72
- key2 = Util.dbl(key1)
73
- [key1, key2]
74
- end
75
- end
76
- end
77
- end
@@ -1,118 +0,0 @@
1
- # encoding: binary
2
- # frozen_string_literal: true
3
-
4
- module Miscreant
5
- module AES
6
- # The AES-SIV misuse resistant authenticated encryption cipher
7
- class SIV
8
- # Generate a new random AES-SIV key of the given size
9
- #
10
- # @param size [Integer] size of key in bytes (32 or 64)
11
- #
12
- # @return [String] newly generated AES-SIV key
13
- def self.generate_key(size = 32)
14
- raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
15
- SecureRandom.random_bytes(size)
16
- end
17
-
18
- # Create a new AES-SIV instance
19
- #
20
- # @param key [String] 32-byte or 64-byte Encoding::BINARY cryptographic key
21
- #
22
- # @return [Miscreant::AES::SIV] new AES-SIV instance
23
- def initialize(key)
24
- raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
25
- raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
26
- raise ArgumentError, "key must be 32 or 64 bytes" unless [32, 64].include?(key.length)
27
-
28
- length = key.length / 2
29
-
30
- @mac_key = key.slice(0, length)
31
- @enc_key = key.slice(length..-1)
32
- end
33
-
34
- # Inspect this AES-SIV instance
35
- #
36
- # @return [String] description of this instance
37
- def inspect
38
- to_s
39
- end
40
-
41
- # Encrypt a message using AES-SIV, authenticating it along with the associated data
42
- #
43
- # @param plaintext [String] an Encoding::BINARY string to encrypt
44
- # @param associated_data [Array<String>] optional array of message headers to authenticate
45
- #
46
- # @return [String] encrypted ciphertext
47
- def seal(plaintext, associated_data = [])
48
- raise TypeError, "expected String, got #{plaintext.class}" unless plaintext.is_a?(String)
49
- raise ArgumentError, "plaintext must be Encoding::BINARY" unless plaintext.encoding == Encoding::BINARY
50
-
51
- v = _s2v(associated_data, plaintext)
52
- ciphertext = _transform(v, plaintext)
53
- v + ciphertext
54
- end
55
-
56
- # Verify and decrypt an AES-SIV ciphertext, authenticating it along with the associated data
57
- #
58
- # @param ciphertext [String] an Encoding::BINARY string to decrypt
59
- # @param associated_data [Array<String>] optional array of message headers to authenticate
60
- #
61
- # @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
62
- # @return [String] decrypted plaintext
63
- def open(ciphertext, associated_data = [])
64
- raise TypeError, "expected String, got #{ciphertext.class}" unless ciphertext.is_a?(String)
65
- raise ArgumentError, "ciphertext must be Encoding::BINARY" unless ciphertext.encoding == Encoding::BINARY
66
-
67
- v = ciphertext.slice(0, AES::BLOCK_SIZE)
68
- ciphertext = ciphertext.slice(AES::BLOCK_SIZE..-1)
69
- plaintext = _transform(v, ciphertext)
70
-
71
- t = _s2v(associated_data, plaintext)
72
- raise IntegrityError, "ciphertext verification failure!" unless Util.ct_equal(t, v)
73
-
74
- plaintext
75
- end
76
-
77
- private
78
-
79
- # Performs raw unauthenticted encryption or decryption of the message
80
- def _transform(v, data)
81
- return "".b if data.empty?
82
-
83
- cipher = OpenSSL::Cipher::AES.new(@mac_key.length * 8, :CTR)
84
- cipher.encrypt
85
- cipher.iv = Util.zero_iv_bits(v)
86
- cipher.key = @enc_key
87
- cipher.update(data) + cipher.final
88
- end
89
-
90
- # The S2V operation consists of the doubling and XORing of the outputs
91
- # of the pseudo-random function CMAC.
92
- #
93
- # See Section 2.4 of RFC 5297 for more information
94
- def _s2v(associated_data, plaintext)
95
- # Note: the standalone S2V returns CMAC(1) if the number of passed
96
- # vectors is zero, however in SIV construction this case is never
97
- # triggered, since we always pass plaintext as the last vector (even
98
- # if it's zero-length), so we omit this case.
99
- cmac = CMAC.new(@mac_key)
100
- d = cmac.digest(AES::ZERO_BLOCK)
101
-
102
- associated_data.each do |ad|
103
- d = Util.dbl(d)
104
- d = Util.xor(d, cmac.digest(ad))
105
- end
106
-
107
- if plaintext.bytesize >= AES::BLOCK_SIZE
108
- d = Util.xorend(plaintext, d)
109
- else
110
- d = Util.dbl(d)
111
- d = Util.xor(d, Util.pad(plaintext, AES::BLOCK_SIZE))
112
- end
113
-
114
- cmac.digest(d)
115
- end
116
- end
117
- end
118
- end
@@ -1,81 +0,0 @@
1
- # encoding: binary
2
- # frozen_string_literal: true
3
-
4
- module Miscreant
5
- # Internal utility functions
6
- module Util
7
- module_function
8
-
9
- # Perform a doubling operation as described in the CMAC and SIV papers
10
- def dbl(value)
11
- overflow = 0
12
- words = value.unpack("N4").reverse
13
-
14
- words.map! do |word|
15
- new_word = (word << 1) & 0xFFFFFFFF
16
- new_word |= overflow
17
- overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
18
- new_word
19
- end
20
-
21
- result = words.reverse.pack("N4")
22
- result[-1] = (result[-1].ord ^ select(overflow, 0x87, 0)).chr
23
- result
24
- end
25
-
26
- # Pad value with a 0x80 value and zeroes up to the given length
27
- def pad(message, length)
28
- padded_length = message.length + length - (message.length % length)
29
- message += "\x80"
30
- message.ljust(padded_length, "\0")
31
- end
32
-
33
- # Perform a constant time(-ish) branch operation
34
- def select(subject, result_if_one, result_if_zero)
35
- (~(subject - 1) & result_if_one) | ((subject - 1) & result_if_zero)
36
- end
37
-
38
- # Perform an xor on arbitrary bytestrings
39
- def xor(a, b)
40
- length = [a.length, b.length].min
41
- output = "\0" * length
42
- length.times do |i|
43
- output[i] = (a[i].ord ^ b[i].ord).chr
44
- end
45
- output
46
- end
47
-
48
- # XOR the second value into the end of the first
49
- def xorend(a, b)
50
- difference = a.length - b.length
51
-
52
- left = a.slice(0, difference)
53
- right = a.slice(difference..-1)
54
-
55
- left + xor(right, b)
56
- end
57
-
58
- # Zero out the top bits in the last 32-bit words of the IV
59
- def zero_iv_bits(iv)
60
- # "We zero-out the top bit in each of the last two 32-bit words
61
- # of the IV before assigning it to Ctr"
62
- # -- http://web.cs.ucdavis.edu/~rogaway/papers/siv.pdf
63
- iv = iv.dup
64
- iv[8] = (iv[8].ord & 0x7f).chr
65
- iv[12] = (iv[12].ord & 0x7f).chr
66
- iv
67
- end
68
-
69
- # Perform a constant time-ish comparison of two bytestrings
70
- def ct_equal(a, b)
71
- return false unless a.bytesize == b.bytesize
72
-
73
- l = a.unpack("C*")
74
- r = 0
75
- i = -1
76
-
77
- b.each_byte { |v| r |= v ^ l[i += 1] }
78
- r.zero?
79
- end
80
- end
81
- end