miscreant 0.1.0 → 0.2.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: 23230bd423fada7ae6bfceeafeb1bff7133e8e75
4
- data.tar.gz: 3dc5b9db54803f64f9e58a87a4ac89370e785b2e
3
+ metadata.gz: 96a35e90aee3d3f35dcbe9a273e57d9685b7d537
4
+ data.tar.gz: dc9ab108d1d8e69daccc528bc6f06f009ab584ed
5
5
  SHA512:
6
- metadata.gz: c53969bd291002daf380591ee6547c49fa24e86e7b3e9c3fdb5c4575cbfa049a1db141af95f447dc8e4bf5a35a9acd053138a4cea1ee940c635cb44e2b0d50a9
7
- data.tar.gz: 45871b8499fbf30d1090a4f45a31eaf6d54583f7e458013c701597a7ec28c2d7df8aac80b94adf717baf1b2b64a9233dc77fd7294595e744b948ae08df7c92c7
6
+ metadata.gz: 8463583471437d1be6ef3885bbcee2ef6d78c58db500d2893a2ab3c91e6864eb5669fae96377d59d6a45e4711b6a2b7bbc57bb8d41f1a8538b0475a10ceeeb7e
7
+ data.tar.gz: f9d0be51f4255fed217290c9d07e90dea204e3009836bf5088eb861881d3b5f84b0d05804004c48d6b50e21f6367a0b8927177dd7cda138173fd1ef82d4a7039
@@ -17,6 +17,9 @@ Metrics/CyclomaticComplexity:
17
17
  Metrics/PerceivedComplexity:
18
18
  Enabled: false
19
19
 
20
+ Metrics/BlockLength:
21
+ Max: 100
22
+
20
23
  Metrics/ClassLength:
21
24
  Max: 100
22
25
 
@@ -30,5 +33,17 @@ Metrics/MethodLength:
30
33
  # Style
31
34
  #
32
35
 
36
+ Style/AsciiComments:
37
+ Enabled: false
38
+
39
+ Style/ClassAndModuleChildren:
40
+ Enabled: false
41
+
42
+ Style/ConditionalAssignment:
43
+ Enabled: false
44
+
45
+ Style/NumericPredicate:
46
+ Enabled: false
47
+
33
48
  Style/StringLiterals:
34
49
  EnforcedStyle: double_quotes
data/README.md CHANGED
@@ -81,27 +81,24 @@ Or install it yourself as:
81
81
 
82
82
  ## API
83
83
 
84
- ### Miscreant::AES::SIV
84
+ ### Miscreant::AEAD
85
85
 
86
- The `Miscreant::AES::SIV` class provides the main interface to the **AES-SIV**
86
+ The `Miscreant::AEAD` class provides the main interface to the **AES-SIV**
87
87
  misuse resistant authenticated encryption function.
88
88
 
89
- To make a new instance, pass in a 32-byte or 64-byte key. Note that these
90
- options are twice the size of what you might be expecting (AES-SIV uses two
91
- AES keys).
92
-
93
- You can generate a random key using the `generate_key` method (default 32 bytes):
89
+ To make a new instance, pass in a binary-encoded 32-byte or 64-byte key.
90
+ Note that these options are twice the size of what you might be expecting
91
+ (AES-SIV uses two AES keys).
94
92
 
95
93
  ```ruby
96
- key_bytes = Miscreant::AES::SIV.generate_key
97
- key = Miscreant::AES::SIV.new(key_bytes)
98
- # => #<Miscreant::AES::SIV:0x007fe0109e85e8>
94
+ secret_key = Miscreant::AEAD.generate_key
95
+ encryptor = Miscreant::AEAD.new("AES-SIV", secret_key)
99
96
  ```
100
97
 
101
98
  #### Encryption (#seal)
102
99
 
103
- The `Miscreant::AES::SIV#seal` method encrypts a message along with a set of
104
- *associated data* message headers.
100
+ The `Miscreant::AEAD#seal` method encrypts a binary-encoded message along with
101
+ a set of *associated data* message headers.
105
102
 
106
103
  It's recommended to include a unique "nonce" value with each message. This
107
104
  prevents those who may be observing your ciphertexts from being able to tell
@@ -114,23 +111,33 @@ Example:
114
111
 
115
112
  ```ruby
116
113
  message = "Hello, world!"
117
- nonce = SecureRandom.random_bytes(16)
114
+ nonce = Miscreant::AEAD.generate_nonce
118
115
  ciphertext = key.seal(message, nonce)
119
116
  ```
120
117
 
121
118
  #### Decryption (#open)
122
119
 
123
- The `Miscreant::AES::SIV#open` method decrypts a ciphertext with the given key.
120
+ The `Miscreant::AEAD#open` method decrypts a binary-encoded ciphertext with the
121
+ given key.
124
122
 
125
123
  Example:
126
124
 
127
125
  ```ruby
128
126
  message = "Hello, world!"
129
- nonce = SecureRandom.random_bytes(16)
127
+ nonce = Miscreant::AEAD.generate_nonce
130
128
  ciphertext = key.seal(message, nonce)
131
129
  plaintext = key.open(ciphertext, nonce)
132
130
  ```
133
131
 
132
+ ## Code of Conduct
133
+
134
+ We abide by the [Contributor Covenant][cc] and ask that you do as well.
135
+
136
+ For more information, please see [CODE_OF_CONDUCT.md].
137
+
138
+ [cc]: https://contributor-covenant.org
139
+ [CODE_OF_CONDUCT.md]: https://github.com/miscreant/miscreant/blob/master/CODE_OF_CONDUCT.md
140
+
134
141
  ## Contributing
135
142
 
136
143
  Bug reports and pull requests are welcome on GitHub at https://github.com/miscreant/miscreant
@@ -1,9 +1,14 @@
1
+ # encoding: binary
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require "openssl"
4
5
  require "securerandom"
5
6
 
6
7
  require "miscreant/version"
8
+ require "miscreant/aead"
9
+ require "miscreant/aes/cmac"
10
+ require "miscreant/aes/pmac"
11
+ require "miscreant/aes/siv"
7
12
  require "miscreant/internals"
8
13
 
9
14
  # Miscreant: A misuse-resistant symmetric encryption library
@@ -13,4 +18,7 @@ module Miscreant
13
18
 
14
19
  # Ciphertext failed to verify as authentic
15
20
  IntegrityError = Class.new(CryptoError)
21
+
22
+ # Hide internals from the outside world
23
+ private_constant :Internals
16
24
  end
@@ -0,0 +1,93 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ # The AEAD class provides Authenticated Encryption with Associated Data
6
+ #
7
+ # If you're looking for the API to encrypt something, congratulations!
8
+ # This is the one you probably want to use. This class provides a high-level
9
+ # interface to Miscreant's misuse-resistant encryption.
10
+ class AEAD
11
+ # Generate a new random AES-SIV key of the given size
12
+ #
13
+ # @param size [Integer] size of key in bytes (32 or 64)
14
+ #
15
+ # @return [String] newly generated AES-SIV key
16
+ def self.generate_key(size = 32)
17
+ raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
18
+ AES::SIV.generate_key(size)
19
+ end
20
+
21
+ # Generate a random "nonce" (i.e. number used once) value
22
+ #
23
+ # @param size [Integer] size of nonce in bytes (default 16)
24
+ #
25
+ # @return [String] newly generated nonce value
26
+ def self.generate_nonce(size = 16)
27
+ SecureRandom.random_bytes(size)
28
+ end
29
+
30
+ # Create a new AEAD encryptor instance.
31
+ #
32
+ # You will need to select an algorithm to use, passed as a string:
33
+ #
34
+ # * "AES-SIV" (RFC 5297): the original AES-SIV function, based on CMAC
35
+ # * "AES-PMAC-SIV": a parallelizable AES-SIV alternative
36
+ #
37
+ # Choose AES-PMAC-SIV if you'd like better performance.
38
+ # Choose AES-SIV if you'd like wider compatibility: AES-PMAC-SIV is
39
+ # presently implemented in the Miscreant libraries.
40
+ #
41
+ # @param alg ["AES-SIV", "AES-PMAC-SIV"] cryptographic algorithm to use
42
+ # @param key [String] 32-byte or 64-byte random Encoding::BINARY secret key
43
+ def initialize(alg, key)
44
+ Internals::Util.validate_bytestring("key", key, length: [32, 64])
45
+
46
+ case alg
47
+ when "AES-SIV", "AES-CMAC-SIV"
48
+ mac = :CMAC
49
+ when "AES-PMAC-SIV"
50
+ mac = :PMAC
51
+ else raise ArgumentError, "unsupported algorithm: #{alg.inspect}"
52
+ end
53
+
54
+ @siv = AES::SIV.new(key, mac)
55
+ end
56
+
57
+ # Inspect this AES-SIV instance
58
+ #
59
+ # @return [String] description of this instance
60
+ def inspect
61
+ to_s
62
+ end
63
+
64
+ # Encrypt a message, authenticating it along with the associated data
65
+ #
66
+ # @param plaintext [String] an Encoding::BINARY string to encrypt
67
+ # @param nonce [String] a unique-per-message value
68
+ # @param ad [String] optional data to authenticate along with the message
69
+ #
70
+ # @return [String] encrypted ciphertext
71
+ def seal(plaintext, nonce:, ad: "")
72
+ raise TypeError, "expected String, got #{nonce.class}" unless nonce.is_a?(String)
73
+ raise TypeError, "expected String, got #{ad.class}" unless ad.is_a?(String)
74
+
75
+ @siv.seal(plaintext, [ad, nonce])
76
+ end
77
+
78
+ # Verify and decrypt a ciphertext, authenticating it along with the associated data
79
+ #
80
+ # @param ciphertext [String] an Encoding::BINARY string to decrypt
81
+ # @param nonce [String] a unique-per-message value
82
+ # @param associated_data [String] optional data to authenticate along with the message
83
+ #
84
+ # @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
85
+ # @return [String] decrypted plaintext
86
+ def open(ciphertext, nonce:, ad: "")
87
+ raise TypeError, "expected nonce to be String, got #{nonce.class}" unless nonce.is_a?(String)
88
+ raise TypeError, "expected ad to be String, got #{ad.class}" unless ad.is_a?(String)
89
+
90
+ @siv.open(ciphertext, [ad, nonce])
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ # The Advanced Encryption Standard
6
+ module AES
7
+ # The AES-CMAC message authentication code
8
+ class CMAC
9
+ # Create a new AES-CMAC instance
10
+ #
11
+ # @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
12
+ def initialize(key)
13
+ @cipher = Internals::AES::BlockCipher.new(key)
14
+
15
+ @subkey1 = Internals::Block.new
16
+ @subkey1.encrypt(@cipher)
17
+ @subkey1.dbl
18
+
19
+ @subkey2 = @subkey1.dup
20
+ @subkey2.dbl
21
+ end
22
+
23
+ # Inspect this AES-CMAC instance
24
+ #
25
+ # @return [String] description of this instance
26
+ def inspect
27
+ to_s
28
+ end
29
+
30
+ # Compute the AES-CMAC of the given input message in a single shot,
31
+ # outputting the MAC tag.
32
+ #
33
+ # Unlike other AES-CMAC implementations, this one does not support
34
+ # incremental processing/IUF operation. (Though that would enable
35
+ # slightly more efficient decryption for AES-SIV)
36
+ #
37
+ # @param message [String] an Encoding::BINARY string to authenticate
38
+ #
39
+ # @return [String] CMAC tag
40
+ def digest(message)
41
+ Internals::Util.validate_bytestring("message", message)
42
+
43
+ if message.empty? || message.length % Internals::Block::SIZE != 0
44
+ message = Internals::Util.pad(message, Internals::Block::SIZE)
45
+ subkey = @subkey2
46
+ else
47
+ subkey = @subkey1
48
+ end
49
+
50
+ count = message.length / Internals::Block::SIZE
51
+ digest = Internals::Block.new
52
+
53
+ count.times do |i|
54
+ digest.xor_in_place(message[Internals::Block::SIZE * i, Internals::Block::SIZE])
55
+ digest.xor_in_place(subkey) if i == count - 1
56
+ digest.encrypt(@cipher)
57
+ end
58
+
59
+ digest.data
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,128 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ # The Advanced Encryption Standard
6
+ module AES
7
+ # The Parallel Message Authentication Code
8
+ class PMAC
9
+ # Number of L blocks to precompute (i.e. µ in the PMAC paper)
10
+ # TODO: dynamically compute these as needed
11
+ PRECOMPUTED_BLOCKS = 31
12
+
13
+ # Create a new PMAC instance
14
+ #
15
+ # @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
16
+ def initialize(key)
17
+ @cipher = Internals::AES::BlockCipher.new(key)
18
+
19
+ # L is defined as follows (quoted from the PMAC paper):
20
+ #
21
+ # Equation 1:
22
+ #
23
+ # a · x =
24
+ # a<<1 if firstbit(a)=0
25
+ # (a<<1) ⊕ 0¹²⁰10000111 if firstbit(a)=1
26
+ #
27
+ # Equation 2:
28
+ #
29
+ # a · x⁻¹ =
30
+ # a>>1 if lastbit(a)=0
31
+ # (a>>1) ⊕ 10¹²⁰1000011 if lastbit(a)=1
32
+ #
33
+ # Let L(0) ← L. For i ∈ [1..µ], compute L(i) ← L(i − 1) · x by
34
+ # Equation (1) using a shift and a conditional xor.
35
+ #
36
+ # Compute L(−1) ← L · x⁻¹ by Equation (2), using a shift and a
37
+ # conditional xor.
38
+ #
39
+ # Save the values L(−1), L(0), L(1), L(2), ..., L(µ) in a table.
40
+ # (Alternatively, [ed: as we have done in this codebase] defer computing
41
+ # some or all of these L(i) values until the value is actually needed.)
42
+ @l = []
43
+ tmp = Internals::Block.new
44
+ tmp.encrypt(@cipher)
45
+
46
+ PRECOMPUTED_BLOCKS.times.each do
47
+ block = Internals::Block.new(tmp.data.dup)
48
+ block.data.freeze
49
+ block.freeze
50
+
51
+ @l << block
52
+ tmp.dbl
53
+ end
54
+
55
+ @l.freeze
56
+
57
+ # Compute L(−1) ← L · x⁻¹:
58
+ #
59
+ # a>>1 if lastbit(a)=0
60
+ # (a>>1) ⊕ 10¹²⁰1000011 if lastbit(a)=1
61
+ #
62
+ @l_inv = Internals::Block.new(@l[0].data.dup)
63
+ last_bit = @l_inv[Internals::Block::SIZE - 1] & 0x01
64
+
65
+ (Internals::Block::SIZE - 1).downto(1) do |i|
66
+ carry = Internals::Util.ct_select(@l_inv[i - 1] & 1, 0x80, 0)
67
+ @l_inv[i] = (@l_inv[i] >> 1) | carry
68
+ end
69
+
70
+ @l_inv[0] >>= 1
71
+ @l_inv[0] ^= Internals::Util.ct_select(last_bit, 0x80, 0)
72
+ @l_inv[Internals::Block::SIZE - 1] ^= Internals::Util.ct_select(last_bit, Internals::Block::R >> 1, 0)
73
+ @l_inv.freeze
74
+ @l_inv.data.freeze
75
+ end
76
+
77
+ # Inspect this PMAC instance
78
+ #
79
+ # @return [String] description of this instance
80
+ def inspect
81
+ to_s
82
+ end
83
+
84
+ # Compute the PMAC of the given input message in a single shot,
85
+ # outputting the MAC tag.
86
+ #
87
+ # Unlike other PMAC implementations, this one does not support
88
+ # incremental processing/IUF operation. (Though that would enable
89
+ # slightly more efficient decryption for AES-SIV)
90
+ #
91
+ # @param message [String] an Encoding::BINARY string to authenticate
92
+ #
93
+ # @return [String] PMAC tag
94
+ def digest(message)
95
+ Internals::Util.validate_bytestring("message", message)
96
+
97
+ offset = Internals::Block.new
98
+ tmp = Internals::Block.new
99
+ tag = Internals::Block.new
100
+ ctr = 0
101
+ remaining = message.bytesize
102
+
103
+ while remaining > Internals::Block::SIZE
104
+ offset.xor_in_place(@l.fetch(Internals::Util.ctz(ctr + 1)))
105
+
106
+ tmp.copy(offset)
107
+ tmp.xor_in_place(message[ctr * Internals::Block::SIZE, Internals::Block::SIZE])
108
+ tmp.encrypt(@cipher)
109
+ tag.xor_in_place(tmp)
110
+
111
+ ctr += 1
112
+ remaining -= Internals::Block::SIZE
113
+ end
114
+
115
+ if remaining == Internals::Block::SIZE
116
+ tag.xor_in_place(message[(ctr * Internals::Block::SIZE)..-1])
117
+ tag.xor_in_place(@l_inv)
118
+ else
119
+ remaining.times { |i| tag[i] ^= message.getbyte((ctr * Internals::Block::SIZE) + i) }
120
+ tag[remaining] ^= 0x80
121
+ end
122
+
123
+ tag.encrypt(@cipher)
124
+ tag.data
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,123 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ # The Advanced Encryption Standard
6
+ module AES
7
+ # The SIV misuse resistant authenticated encryption mode
8
+ #
9
+ # This class is intended for power users. If you're uncertain if you
10
+ # should be using it, you probably want the `Miscreant::AEAD` API.
11
+ class SIV
12
+ # Generate a new random AES-SIV key of the given size
13
+ #
14
+ # @param size [Integer] size of key in bytes (32 or 64)
15
+ #
16
+ # @return [String] newly generated AES-SIV key
17
+ def self.generate_key(size = 32)
18
+ raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
19
+ SecureRandom.random_bytes(size)
20
+ end
21
+
22
+ # Create a new AES-SIV instance
23
+ #
24
+ # @param key [String] 32-byte or 64-byte Encoding::BINARY cryptographic key
25
+ # @param mac [:CMAC, :PMAC] (optional) MAC function to use (default CMAC)
26
+ def initialize(key, mac_class = :CMAC)
27
+ Internals::Util.validate_bytestring("key", key, length: [32, 64])
28
+ length = key.length / 2
29
+
30
+ case mac_class
31
+ when :CMAC
32
+ @mac = CMAC.new(key[0, length])
33
+ when :PMAC
34
+ @mac = PMAC.new(key[0, length])
35
+ else raise ArgumentError, "bad MAC class: #{mac_class} (expected :CMAC or :PMAC)"
36
+ end
37
+
38
+ @ctr = Internals::AES::CTR.new(key[length..-1])
39
+ end
40
+
41
+ # Inspect this AES-SIV instance
42
+ #
43
+ # @return [String] description of this instance
44
+ def inspect
45
+ to_s
46
+ end
47
+
48
+ # Encrypt a message using AES-SIV, authenticating it along with the associated data
49
+ #
50
+ # @param plaintext [String] an Encoding::BINARY string to encrypt
51
+ # @param associated_data [Array<String>] optional array of message headers to authenticate
52
+ #
53
+ # @return [String] encrypted ciphertext
54
+ def seal(plaintext, associated_data = [])
55
+ raise TypeError, "expected String, got #{plaintext.class}" unless plaintext.is_a?(String)
56
+ v = _s2v(associated_data, plaintext)
57
+ ciphertext = @ctr.encrypt(_zero_iv_bits(v), plaintext)
58
+ v + ciphertext
59
+ end
60
+
61
+ # Verify and decrypt an AES-SIV ciphertext, authenticating it along with the associated data
62
+ #
63
+ # @param ciphertext [String] an Encoding::BINARY string to decrypt
64
+ # @param associated_data [Array<String>] optional array of message headers to authenticate
65
+ #
66
+ # @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
67
+ # @return [String] decrypted plaintext
68
+ def open(ciphertext, associated_data = [])
69
+ raise TypeError, "expected String, got #{ciphertext.class}" unless ciphertext.is_a?(String)
70
+ v = ciphertext[0, Internals::Block::SIZE]
71
+ plaintext = @ctr.encrypt(_zero_iv_bits(v), ciphertext[Internals::Block::SIZE..-1])
72
+ t = _s2v(associated_data, plaintext)
73
+ raise IntegrityError, "ciphertext verification failure!" unless Internals::Util.ct_equal(t, v)
74
+
75
+ plaintext
76
+ end
77
+
78
+ private
79
+
80
+ # The S2V operation consists of the doubling and XORing of the outputs
81
+ # of the pseudo-random function CMAC.
82
+ #
83
+ # See Section 2.4 of RFC 5297 for more information
84
+ def _s2v(associated_data, plaintext)
85
+ # Note: the standalone S2V returns CMAC(1) if the number of passed
86
+ # vectors is zero, however in SIV construction this case is never
87
+ # triggered, since we always pass plaintext as the last vector (even
88
+ # if it's zero-length), so we omit this case.
89
+ d = Internals::Block.new
90
+ d.xor_in_place(@mac.digest(d.data))
91
+
92
+ associated_data.each do |ad|
93
+ d.dbl
94
+ d.xor_in_place(@mac.digest(ad))
95
+ end
96
+
97
+ if plaintext.bytesize >= Internals::Block::SIZE
98
+ # TODO: implement this more efficiently by adding IUF support to CMAC
99
+ difference = plaintext.length - Internals::Block::SIZE
100
+ beginning = plaintext[0, difference]
101
+ d.xor_in_place(plaintext[difference..-1])
102
+ msg = beginning + d.data
103
+ else
104
+ d.dbl
105
+ d.xor_in_place(Internals::Util.pad(plaintext, Internals::Block::SIZE))
106
+ msg = d.data
107
+ end
108
+
109
+ @mac.digest(msg)
110
+ end
111
+
112
+ # "We zero-out the top bit in each of the last two 32-bit words
113
+ # of the IV before assigning it to Ctr"
114
+ # -- http://web.cs.ucdavis.edu/~rogaway/papers/siv.pdf
115
+ def _zero_iv_bits(iv)
116
+ iv = iv.dup
117
+ iv.setbyte(8, iv.getbyte(8) & 0x7f)
118
+ iv.setbyte(12, iv.getbyte(12) & 0x7f)
119
+ iv
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,9 +1,10 @@
1
+ # encoding: binary
1
2
  # frozen_string_literal: true
2
3
 
3
- require "miscreant/internals/aes"
4
- require "miscreant/internals/aes/siv"
5
- require "miscreant/internals/aes/cmac"
4
+ require "miscreant/internals/block"
6
5
  require "miscreant/internals/util"
6
+ require "miscreant/internals/aes/block_cipher"
7
+ require "miscreant/internals/aes/ctr"
7
8
 
8
9
  module Miscreant
9
10
  # Internal functionality not intended for direct consumption
@@ -0,0 +1,48 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ module Internals
6
+ module AES # :nodoc:
7
+ # The AES cipher in a raw block mode (a.k.a. ECB mode)
8
+ #
9
+ # NOTE: The only valid use of ECB mode is constructing higher-level
10
+ # cryptographic primitives. This library uses this class to implement
11
+ # the CMAC and PMAC message authentication codes.
12
+ class BlockCipher # :nodoc:
13
+ # Create a new block cipher instance
14
+ #
15
+ # @param key [String] a random 16-byte or 32-byte Encoding::BINARY encryption key
16
+ #
17
+ # @raise [TypeError] the key was not a String
18
+ # @raise [ArgumentError] the key was the wrong length or encoding
19
+ def initialize(key)
20
+ Util.validate_bytestring("key", key, length: [16, 32])
21
+
22
+ @cipher = OpenSSL::Cipher.new("AES-#{key.length * 8}-ECB")
23
+ @cipher.encrypt
24
+ @cipher.padding = 0
25
+ @cipher.key = key
26
+ end
27
+
28
+ # Inspect this AES block cipher instance
29
+ #
30
+ # @return [String] description of this instance
31
+ def inspect
32
+ to_s
33
+ end
34
+
35
+ # Encrypt the given AES block-sized message
36
+ #
37
+ # @param message [String] a 16-byte Encoding::BINARY message to encrypt
38
+ #
39
+ # @raise [TypeError] the message was not a String
40
+ # @raise [ArgumentError] the message was the wrong length
41
+ def encrypt(message)
42
+ Util.validate_bytestring("message", message, length: Block::SIZE)
43
+ @cipher.update(message) + @cipher.final
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ module Internals
6
+ module AES # :nodoc:
7
+ # The AES-CTR unauthenticated stream cipher
8
+ class CTR # :nodoc:
9
+ # Create a new AES-CTR instance
10
+ #
11
+ # @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
12
+ def initialize(key)
13
+ Util.validate_bytestring("key", key, length: [16, 32])
14
+ @cipher = OpenSSL::Cipher::AES.new(key.bytesize * 8, :CTR)
15
+ @cipher.encrypt
16
+ @cipher.key = key
17
+ end
18
+
19
+ # Inspect this AES-CTR instance
20
+ #
21
+ # @return [String] description of this instance
22
+ def inspect
23
+ to_s
24
+ end
25
+
26
+ # Encrypt the given message using the given counter (i.e. IV)
27
+ #
28
+ # @param iv [String] initial counter value as a 16-byte Encoding::BINARY string
29
+ # @param message [String] message to be encrypted
30
+ def encrypt(iv, message)
31
+ Util.validate_bytestring("IV", iv, length: Block::SIZE)
32
+ return "".b if message.empty?
33
+
34
+ @cipher.iv = iv
35
+ @cipher.update(message) + @cipher.final
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,103 @@
1
+ # encoding: binary
2
+ # frozen_string_literal: true
3
+
4
+ module Miscreant
5
+ module Internals
6
+ # A 128-bit block (i.e. for AES)
7
+ class Block # :nodoc:
8
+ # Size of an AES block in bytes
9
+ SIZE = 16
10
+
11
+ # Minimal irreducible polynomial for a 128-bit block size
12
+ R = 0x87
13
+
14
+ attr_reader :data
15
+
16
+ # Create a new Block, optionally from the given data
17
+ def initialize(data = nil)
18
+ if data
19
+ @data = Util.validate_bytestring("block data", data, length: SIZE)
20
+ else
21
+ @data = "\0".b * SIZE
22
+ end
23
+ end
24
+
25
+ # Inspect the contents of the block in hex
26
+ def inspect
27
+ "#<#{self.class} data:\"#{@data.unpack('H*').first}\">"
28
+ end
29
+
30
+ # Retrieve the value of the byte at the given index as an integer
31
+ def [](n)
32
+ raise IndexError, "n must be zero or greater (got #{n})" if n < 0
33
+ raise IndexError, "n must be less than #{SIZE} (got #{n})" unless n < SIZE
34
+
35
+ @data.getbyte(n)
36
+ end
37
+
38
+ # Set the value of the byte at the given index as an integer
39
+ def []=(n, byte)
40
+ @data.setbyte(n, byte)
41
+ end
42
+
43
+ # Reset the value of this block to all zeroes
44
+ def clear
45
+ SIZE.times { |n| @data[n] = 0 }
46
+ end
47
+
48
+ # Copy the contents of another block into this block
49
+ def copy(other_block)
50
+ SIZE.times { |n| @data[n] = other_block.data[n] }
51
+ end
52
+
53
+ # Double a value over GF(2^128):
54
+ #
55
+ # a<<1 if firstbit(a)=0
56
+ # (a<<1) ⊕ 0¹²⁰10000111 if firstbit(a)=1
57
+ #
58
+ def dbl
59
+ overflow = 0
60
+ words = @data.unpack("N4").reverse
61
+
62
+ words.map! do |word|
63
+ new_word = (word << 1) & 0xFFFFFFFF
64
+ new_word |= overflow
65
+ overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
66
+ new_word
67
+ end
68
+
69
+ @data = words.reverse.pack("N4")
70
+ @data[-1] = (@data[-1].ord ^ Util.ct_select(overflow, R, 0)).chr
71
+ self
72
+ end
73
+
74
+ # Encrypt this block in-place, replacing its current contents with
75
+ # their ciphertext under the given block cipher
76
+ #
77
+ # @param cipher [Miscreant::Internals::AES::BlockCipher] block cipher to encrypt with
78
+ def encrypt(cipher)
79
+ raise TypeError, "invalid cipher: #{cipher.class}" unless cipher.is_a?(AES::BlockCipher)
80
+
81
+ # TODO: more efficient in-place encryption
82
+ @data = cipher.encrypt(@data)
83
+ end
84
+
85
+ # XOR the given data into the current block in-place
86
+ #
87
+ # @param value [AES::Block, String] a block or String to XOR into this one
88
+ def xor_in_place(value)
89
+ case value
90
+ when Block
91
+ value = value.data
92
+ when String
93
+ Util.validate_bytestring("value", value, length: SIZE)
94
+ else raise TypeError, "invalid XOR input: #{value.class}"
95
+ end
96
+
97
+ SIZE.times do |i|
98
+ self[i] ^= value.getbyte(i)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -7,76 +7,72 @@ module Miscreant
7
7
  module Util # :nodoc:
8
8
  module_function
9
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
10
+ # :nodoc: Lookup table for the number of trailing zeroes in a byte
11
+ CTZ_TABLE = [
12
+ 8, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
13
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
14
+ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
15
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
16
+ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
17
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
18
+ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
19
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
20
+ 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
21
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
22
+ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
23
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
24
+ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
25
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
26
+ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
27
+ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
28
+ ].freeze
14
29
 
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
30
+ # Perform a constant time-ish comparison of two bytestrings
31
+ def ct_equal(a, b)
32
+ return false unless a.bytesize == b.bytesize
21
33
 
22
- result = words.reverse.pack("N4")
23
- result[-1] = (result[-1].ord ^ select(overflow, 0x87, 0)).chr
24
- result
25
- end
34
+ l = a.unpack("C*")
35
+ r = 0
36
+ i = -1
26
37
 
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")
38
+ b.each_byte { |v| r |= v ^ l[i += 1] }
39
+ r.zero?
32
40
  end
33
41
 
34
42
  # Perform a constant time(-ish) branch operation
35
- def select(subject, result_if_one, result_if_zero)
43
+ def ct_select(subject, result_if_one, result_if_zero)
36
44
  (~(subject - 1) & result_if_one) | ((subject - 1) & result_if_zero)
37
45
  end
38
46
 
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)
47
+ # Count the number of zeros in a given 8-bit integer
48
+ #
49
+ # @param value [Integer] an integer in the 8-bit unsigned range (0-255)
50
+ def ctz(value)
51
+ CTZ_TABLE.fetch(value)
57
52
  end
58
53
 
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
54
+ # Pad value with a 0x80 value and zeroes up to the given length
55
+ def pad(message, length)
56
+ padded_length = message.length + length - (message.length % length)
57
+ message += "\x80"
58
+ message.ljust(padded_length, "\0")
68
59
  end
69
60
 
70
- # Perform a constant time-ish comparison of two bytestrings
71
- def ct_equal(a, b)
72
- return false unless a.bytesize == b.bytesize
61
+ # Ensure a string is a valid bytestring (potentially of a given length)
62
+ def validate_bytestring(name, string, length: nil)
63
+ raise TypeError, "expected String for #{name}, got #{string.class}" unless string.is_a?(String)
64
+ raise ArgumentError, "#{name} must be Encoding::BINARY" unless string.encoding == Encoding::BINARY
73
65
 
74
- l = a.unpack("C*")
75
- r = 0
76
- i = -1
66
+ case length
67
+ when Array
68
+ raise ArgumentError, "#{name} must be #{length.join(' or ')} bytes long" unless length.include?(string.bytesize)
69
+ when Integer
70
+ raise ArgumentError, "#{name} must be #{length}-bytes long" unless string.bytesize == length
71
+ else
72
+ raise TypeError, "bad length parameter: #{length.inspect} (#{length.class}" unless length.nil?
73
+ end
77
74
 
78
- b.each_byte { |v| r |= v ^ l[i += 1] }
79
- r.zero?
75
+ string
80
76
  end
81
77
  end
82
78
  end
@@ -1,5 +1,6 @@
1
+ # encoding: binary
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module Miscreant
4
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
5
6
  end
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.1.0
4
+ version: 0.2.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-31 00:00:00.000000000 Z
11
+ date: 2017-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -39,10 +39,14 @@ files:
39
39
  - README.md
40
40
  - Rakefile
41
41
  - lib/miscreant.rb
42
+ - lib/miscreant/aead.rb
43
+ - lib/miscreant/aes/cmac.rb
44
+ - lib/miscreant/aes/pmac.rb
45
+ - lib/miscreant/aes/siv.rb
42
46
  - lib/miscreant/internals.rb
43
- - lib/miscreant/internals/aes.rb
44
- - lib/miscreant/internals/aes/cmac.rb
45
- - lib/miscreant/internals/aes/siv.rb
47
+ - lib/miscreant/internals/aes/block_cipher.rb
48
+ - lib/miscreant/internals/aes/ctr.rb
49
+ - lib/miscreant/internals/block.rb
46
50
  - lib/miscreant/internals/util.rb
47
51
  - lib/miscreant/version.rb
48
52
  - miscreant.gemspec
@@ -1,15 +0,0 @@
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
@@ -1,79 +0,0 @@
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
@@ -1,120 +0,0 @@
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