miscreant 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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